// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {DialogType} from '../prod/file_manager/shared_types.js';
import {addEntries, ENTRIES, EntryType, RootPath, sendBrowserTestCommand, sendTestMessage, TestEntryInfo} from '../test_util.js';
import {remoteCall} from './background.js';
import {DirectoryTreePageObject} from './page_objects/directory_tree.js';
import {BASIC_ANDROID_ENTRY_SET, BASIC_LOCAL_ENTRY_SET, FakeTask} from './test_data.js';
/**
* Copies or moves a file from Downloads to the provided location.
* @param appId ID of the Files app window.
* @param file Test entry info to be copied/cut.
* @param destination Name of the destination folder.
* @param isCopy Whether it should copy or move the file.
* @return Promise fulfilled on success.
*/
async function copyOrMove(
appId: string, file: TestEntryInfo, destination: string,
isCopy: boolean): Promise<void> {
if (!file || !file.nameText || !destination) {
chrome.test.assertTrue(false, 'copyOrMove invalid parameters');
}
const directoryTree = await DirectoryTreePageObject.create(appId);
await directoryTree.navigateToPath('/My files/Downloads');
await remoteCall.waitForFiles(appId, [file.getExpectedRow()]);
await remoteCall.waitUntilSelected(appId, file.nameText);
const command = isCopy ? 'copy' : 'cut';
await remoteCall.callRemoteTestUtil('execCommand', appId, [command]);
await directoryTree.navigateToPath(destination);
await remoteCall.callRemoteTestUtil('execCommand', appId, ['paste']);
}
/**
* List of panel types.
* Keep this in sync with PanelItem panel types.
*/
enum PanelType {
DEFAULT = -1,
PROGRESS = 0,
SUMMARY = 1,
DONE = 2,
ERROR = 3,
INFO = 4,
FORMAT_PROGRESS = 5,
SYNC_PROGRESS = 6,
}
/**
* List of checked panel status indicator types.
*/
enum StatusIndicator {
WARNING = 'warning',
FAILURE = 'failure',
}
/**
* Returns the first panel item with the provided panel type.
* @param appId ID of the Files app window.
*/
async function getPanelItem(appId: string, panelType: PanelType) {
const panel = await remoteCall.waitForElement(
appId, ['#progress-panel', `xf-panel-item[panel-type="${panelType}"]`]);
return panel;
}
/**
* Checks that the panel item with provided parameters exists.
* @param appId ID of the Files app window.
* @param panelType Expected panel type.
* @param primaryText Expected primary text.
* @param secondaryText Expected secondary text. Can be null.
* @param status Expected status indicator (failure or warning).
* @return Promise fulfilled on success.
*/
async function verifyPanelItem(
appId: string, panelType: PanelType, primaryText: string,
secondaryText: null|string, status: StatusIndicator): Promise<void> {
const panel = await getPanelItem(appId, panelType);
chrome.test.assertEq(primaryText, panel.attributes['primary-text']);
chrome.test.assertEq(secondaryText, panel.attributes['secondary-text']);
chrome.test.assertEq('status', panel.attributes['indicator']);
chrome.test.assertEq(status, panel.attributes['status']);
}
/**
* Checks that the panel item's primary and secondary buttons have expected type
* and text, and then clicks the button defined by selectedButton.
* @param appId ID of the Files app window.
* @param secondaryButtonCategory Expected secondary button category (dismiss or
* cancel).
* @param selectedButton The button to click (primary or secondary).
*/
async function verifyPanelButtonsAndClick(
appId: string, secondaryButtonCategory: string, selectedButton: string) {
const primaryButton = await remoteCall.waitForElement(
appId, ['#progress-panel', 'xf-panel-item', 'xf-button#primary-action']);
chrome.test.assertEq(
'extra-button', primaryButton.attributes['data-category']);
const secondaryButton = await remoteCall.waitForElement(
appId,
['#progress-panel', 'xf-panel-item', 'xf-button#secondary-action']);
chrome.test.assertEq(
secondaryButtonCategory, secondaryButton.attributes['data-category']);
await remoteCall.waitAndClickElement(appId, [
'#progress-panel',
'xf-panel-item',
`xf-button#${selectedButton}-action`,
]);
}
/**
* Expands the summary panel if it's collapsed, no-op if already expanded.
* @param appId ID of the Files app window.
* */
async function maybeExpandSummary(appId: string) {
const summaryPanel = await getPanelItem(appId, PanelType.SUMMARY);
if (summaryPanel.attributes['data-category'] === 'expanded') {
return;
}
await remoteCall.waitAndClickElement(appId, [
'#progress-panel',
`xf-panel-item[panel-type="${PanelType.SUMMARY}"]`,
'xf-button#primary-action',
]);
await remoteCall.waitForElement(appId, [
'#progress-panel',
`xf-panel-item[panel-type="${
PanelType.SUMMARY}"][data-category="expanded"]`,
]);
}
/**
* Tests that DLP block toast is shown when a restricted file is cut.
*/
export async function transferShowDlpToast() {
const entry = ENTRIES.hello;
// Open Files app.
const appId =
await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);
// Setup the restrictions.
await sendTestMessage({
name: 'setBlockedFilesTransfer',
fileNames: [entry.nameText],
});
// Mount a USB volume.
await sendTestMessage({name: 'mountFakeUsbEmpty'});
// Wait for the USB volume to mount.
const directoryTree = await DirectoryTreePageObject.create(appId);
await directoryTree.waitForItemByType('removable');
// Cut and paste the file.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ false);
// Check: a toast should be displayed because cut is disallowed.
await remoteCall.waitForElement(appId, '#toast');
// Navigate back to Downloads.
await directoryTree.navigateToPath('/My files/Downloads');
// The file should be there because the transfer was restricted.
await remoteCall.waitUntilSelected(appId, entry.nameText);
}
/**
* Tests that if the file is restricted by DLP, a managed icon is shown in the
* detail list and a tooltip is displayed when hovering over that icon.
*/
export async function dlpShowManagedIcon() {
// Add entries to Downloads and setup the fake source URLs.
await addEntries(['local'], BASIC_LOCAL_ENTRY_SET);
await sendTestMessage({
name: 'setGetFilesSourcesMock',
fileNames: BASIC_LOCAL_ENTRY_SET.map(e => e.nameText),
sourceUrls: [
'https://blocked.com',
'https://allowed.com',
'https://blocked.com',
'https://warned.com',
'https://not-set.com',
],
});
// Setup the restrictions.
await sendTestMessage({name: 'setIsRestrictedByAnyRuleRestrictions'});
// Open Files app.
const appId = await remoteCall.setupAndWaitUntilReady(
RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
const dlpManagedIcon = '#file-list .dlp-managed-icon.is-dlp-restricted';
// Check: only three of the five files should have the 'dlp-managed-icon'
// class, which means that the icon is displayed.
await remoteCall.waitForElementsCount(appId, [dlpManagedIcon], 3);
// Hover over an icon: a tooltip should appear.
chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
'fakeMouseOver', appId, [dlpManagedIcon]));
// Check: the DLP managed icon tooltip should be visible. The full text
// contains a placeholder for the link so here we only check the first part.
const labelTextPrefix = 'This file is confidential and subject ' +
'to restrictions by administrator policy.';
const label = await remoteCall.waitForElement(
appId, ['files-tooltip[visible=true]', '#label']);
chrome.test.assertTrue((label.text ?? '').startsWith(labelTextPrefix));
}
/**
* Tests that if the file is restricted by DLP, the Restriction details context
* menu item appears and is enabled.
*/
export async function dlpContextMenuRestrictionDetails() {
// Add entries to Downloads and setup the fake source URLs.
const entry = ENTRIES.hello;
await addEntries(['local'], [entry]);
await sendTestMessage({
name: 'setGetFilesSourcesMock',
fileNames: [entry.nameText],
sourceUrls: ['https://blocked.com'],
});
// Setup the restrictions.
await sendTestMessage({name: 'setIsRestrictedByAnyRuleBlocked'});
// Open Files app.
const appId =
await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);
// Wait for the DLP managed icon to be shown - this also means metadata has
// been cached and can be used to show the context menu command.
await remoteCall.waitForElementsCount(
appId, ['#file-list .dlp-managed-icon.is-dlp-restricted'], 1);
// Select the file.
await remoteCall.waitUntilSelected(appId, entry.nameText);
// Right-click on the file.
await remoteCall.waitAndRightClick(appId, ['.table-row[selected]']);
// Wait for the context menu to appear.
await remoteCall.waitForElement(appId, '#file-context-menu:not([hidden])');
// Wait for the context menu command option to appear.
await remoteCall.waitForElement(
appId,
'#file-context-menu:not([hidden])' +
' [command="#dlp-restriction-details"]' +
':not([hidden]):not([disabled])');
}
// Filters used for the following save-as and file-open tests.
// Rows in `My files`
const downloadsRow = ['Downloads', '--', 'Folder'];
const playFilesRow = ['Play files', '--', 'Folder'];
const linuxFilesRow = ['Linux files', '--', 'Folder'];
// Dialog buttons
const okButton = '.button-panel button.ok:enabled';
const disabledOkButton = '.button-panel button.ok:disabled';
const cancelButton = '.button-panel button.cancel';
/**
* Tests the save dialogs properly show DLP blocked Play files, before and after
* being mounted, both in the navigation list and in the details list.
*/
export async function saveAsDlpRestrictedAndroid() {
// Setup the restrictions.
await sendTestMessage({name: 'setBlockedComponent', component: 'arc'});
const closer = async (dialog: string) => {
// Select My Files folder and wait for file list to display Downloads, Play
// files, and Linux files.
const directoryTree = await DirectoryTreePageObject.create(dialog);
await directoryTree.navigateToPath('/My files');
await remoteCall.waitForFiles(
dialog, [downloadsRow, playFilesRow, linuxFilesRow],
{ignoreFileSize: true, ignoreLastModifiedTime: true});
// Only one directory, Android files, should be disabled, both as the tree
// item and the directory in the main list.
const guestName = 'Play files';
const disabledDirectory = `.directory[disabled][file-name="${guestName}"]`;
await remoteCall.waitForElement(dialog, disabledDirectory);
const realTreeItem = await directoryTree.waitForItemByType('android_files');
directoryTree.assertItemDisabled(realTreeItem);
// Verify that the button is enabled when a non-blocked volume is selected.
await remoteCall.waitUntilSelected(dialog, 'Downloads');
await remoteCall.waitForElement(dialog, okButton);
// Verify that the button is disabled when a blocked volume is selected.
await remoteCall.waitUntilSelected(dialog, guestName);
await remoteCall.waitForElement(dialog, disabledOkButton);
// Unmount Play files and mount ARCVM.
await sendTestMessage({name: 'unmountPlayFiles'});
await sendTestMessage({
name: 'registerMountableGuest',
displayName: guestName,
canMount: true,
vmType: 'arcvm',
});
// Wait for the placeholder "Play files" to appear, the directory tree item
// should be disabled, but the file row shouldn't be disabled.
const fakeTreeItem =
await directoryTree.waitForPlaceholderItemByType('android_files');
directoryTree.assertItemDisabled(fakeTreeItem);
await directoryTree.selectPlaceholderItemByType('android_files');
await remoteCall.waitForFiles(
dialog, [downloadsRow, playFilesRow, linuxFilesRow],
{ignoreFileSize: true, ignoreLastModifiedTime: true});
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'saveFile'}, 'downloads', [], closer));
}
/**
* Tests the save dialogs properly show DLP blocked guest OS volumes, before and
* after being mounted: it should be marked as disabled in the navigation list
* both before and after mounting, but in the file list it will only be disabled
* after mounting.
*/
export async function saveAsDlpRestrictedVm() {
// Setup the restrictions.
await sendTestMessage({name: 'setBlockedComponent', component: 'pluginVm'});
const guestName = 'JennyAnyDots';
const guestId = await sendTestMessage({
name: 'registerMountableGuest',
displayName: guestName,
canMount: true,
vmType: 'bruschetta',
});
const closer = async (dialog: string) => {
// Select My Files folder and wait for file list.
const directoryTree = await DirectoryTreePageObject.create(dialog);
await directoryTree.navigateToPath('/My files');
const guestFilesRow = [guestName, '--', 'Folder'];
await remoteCall.waitForFiles(
dialog, [downloadsRow, playFilesRow, linuxFilesRow, guestFilesRow],
{ignoreFileSize: true, ignoreLastModifiedTime: true});
const directory = `.directory:not([disabled])[file-name="${guestName}"]`;
const disabledDirectory = `.directory[disabled][file-name="${guestName}"]`;
// Before mounting, the guest should be disabled in the navigation list, but
// not in the file list.
let fakeTreeItem =
await directoryTree.waitForPlaceholderItemByType('bruschetta');
directoryTree.assertItemDisabled(fakeTreeItem);
await remoteCall.waitForElementsCount(dialog, [directory], 1);
// Mount the guest by selecting it in the file list.
await remoteCall.waitUntilSelected(dialog, guestName);
await remoteCall.waitAndClickElement(dialog, [okButton]);
// Verify that the guest is mounted and disabled, now both in the navigation
// and the file list, as well as that the OK button is disabled while we're
// still in the guest directory.
await remoteCall.waitUntilCurrentDirectoryIsChanged(
dialog, `/My files/${guestName}`);
await remoteCall.waitForElement(dialog, disabledOkButton);
await directoryTree.navigateToPath('/My files');
const realTreeItem = await directoryTree.waitForItemByType('bruschetta');
directoryTree.assertItemDisabled(realTreeItem);
await remoteCall.waitForElementsCount(dialog, [disabledDirectory], 1);
await remoteCall.waitUntilSelected(dialog, guestName);
await remoteCall.waitForElement(dialog, disabledOkButton);
// Unmount the volume.
await sendTestMessage({
name: 'unmountGuest',
guestId: guestId,
});
// Verify that volume is replaced by the fake and is still disabled.
fakeTreeItem =
await directoryTree.waitForPlaceholderItemByType('bruschetta');
directoryTree.assertItemDisabled(fakeTreeItem);
await directoryTree.waitForItemLostByType('bruschetta');
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'saveFile'}, 'downloads', [], closer));
}
/**
* Tests the save dialogs properly show DLP blocked Linux files, before and
* after being mounted: it should be marked as disabled in the navigation list
* both before and after mounting, but in the file list it will only be disabled
* after mounting.
*/
export async function saveAsDlpRestrictedCrostini() {
// Setup the restrictions.
await sendTestMessage({name: 'setBlockedComponent', component: 'crostini'});
// Add entries to Downloads.
await addEntries(['local'], [ENTRIES.hello]);
const closer = async (dialog: string) => {
// Verify that the button is enabled when a file is selected.
await remoteCall.waitUntilSelected(dialog, ENTRIES.hello.targetPath);
await remoteCall.waitForElement(dialog, okButton);
// Select My Files folder and wait for file list to display Downloads, Play
// files, and Linux files.
const directoryTree = await DirectoryTreePageObject.create(dialog);
await directoryTree.navigateToPath('/My files');
await remoteCall.waitForFiles(
dialog, [downloadsRow, playFilesRow, linuxFilesRow],
{ignoreFileSize: true, ignoreLastModifiedTime: true});
const directory = '.directory:not([disabled])[file-name="Linux files"]';
const disabledDirectory = '.directory[disabled][file-name="Linux files"]';
// Before mounting, Linux files should be disabled in the navigation list,
// but not in the file list.
await remoteCall.waitForElementsCount(dialog, [directory], 1);
const fakeTreeItem =
await directoryTree.waitForPlaceholderItemByType('crostini');
directoryTree.assertItemDisabled(fakeTreeItem);
// Mount Crostini by selecting it in the file list. We cannot select/mount
// it from the navigation list since it's already disabled there.
await remoteCall.waitUntilSelected(dialog, 'Linux files');
await remoteCall.waitAndClickElement(dialog, [okButton]);
// Verify that Crostini is mounted and disabled, now both in the navigation
// and the file list, as well as that the OK button is disabled while we're
// still in the Linux files directory.
await remoteCall.waitUntilCurrentDirectoryIsChanged(dialog, '/Linux files');
await remoteCall.waitForElement(dialog, disabledOkButton);
await directoryTree.navigateToPath('/My files');
const realTreeItem = await directoryTree.waitForItemByType('crostini');
directoryTree.assertItemDisabled(realTreeItem);
await remoteCall.waitForElementsCount(dialog, [disabledDirectory], 1);
await remoteCall.waitUntilSelected(dialog, 'Linux files');
await remoteCall.waitForElement(dialog, disabledOkButton);
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'saveFile'}, 'downloads', [ENTRIES.hello], closer));
}
/**
* Tests the save dialogs properly show blocked USB volumes.
*/
export async function saveAsDlpRestrictedUsb() {
// Mount a USB volume.
await sendTestMessage({name: 'mountFakeUsbEmpty'});
// Setup the restrictions.
await sendTestMessage({name: 'setBlockedComponent', component: 'usb'});
const closer = async (dialog: string) => {
const directoryTree = await DirectoryTreePageObject.create(dialog);
// It should be disabled in the navigation list, but the eject button should
// be enabled.
let realTreeItem = await directoryTree.waitForItemByType('removable');
directoryTree.assertItemDisabled(realTreeItem);
const ejectButton =
await directoryTree.waitForItemEjectButtonByType('removable');
chrome.test.assertEq(undefined, ejectButton.attributes['disabled']);
// Unmount.
await sendTestMessage({name: 'unmountUsb'});
await directoryTree.waitForItemLostByType('removable');
// Mount again - should still be disabled.
await sendTestMessage({name: 'mountFakeUsbEmpty'});
realTreeItem = await directoryTree.waitForItemByType('removable');
directoryTree.assertItemDisabled(realTreeItem);
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'saveFile'}, 'downloads', [], closer));
}
/**
* Tests the save dialogs properly show blocked Google drive volume.
*/
export async function saveAsDlpRestrictedDrive() {
// Setup the restrictions.
await sendTestMessage({name: 'setBlockedComponent', component: 'drive'});
const closer = async (dialog: string) => {
const directoryTree = await DirectoryTreePageObject.create(dialog);
// It should be disabled in the navigation list, and the expand icon
// shouldn't be visible.
const treeItem = await directoryTree.waitForItemToHaveChildrenByLabel(
'Google Drive', /* hasChildren= */ false);
directoryTree.assertItemDisabled(treeItem);
await directoryTree.waitForItemExpandIconToHideByLabel('Google Drive');
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'saveFile'}, 'downloads', [], closer));
}
/**
* Tests that save dialogs are opened in a requested volume/directory, when it's
* not blocked by DLP. This test is an addition to the
* `saveAsDlpRestrictedRedirectsToMyFiles` test case, which assert that if the
* directory is blocked, the dialog will not be opened in the requested path.
*/
export async function saveAsNonDlpRestricted() {
// Add entries to Play files.
await addEntries(['android_files'], BASIC_ANDROID_ENTRY_SET);
const allowedCloser = async (dialog: string) => {
// Double check: current directory should be Play files.
await remoteCall.waitUntilCurrentDirectoryIsChanged(
dialog, '/My files/Play files');
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
// Open a save dialog in Play Files.
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'saveFile'}, 'android_files', BASIC_ANDROID_ENTRY_SET,
allowedCloser));
}
/**
* Tests that save dialogs are never opened in a DLP blocked volume/directory,
* but rather in the default display root.
*/
export async function saveAsDlpRestrictedRedirectsToMyFiles() {
// Add entries to Downloads and Play files.
await addEntries(['local'], [ENTRIES.hello]);
await addEntries(['android_files'], BASIC_ANDROID_ENTRY_SET);
// Setup the restrictions.
await sendTestMessage({name: 'setBlockedComponent', component: 'arc'});
const blockedCloser = async (dialog: string) => {
// Double check: current directory should be the default root, not Play
// files.
await remoteCall.waitUntilCurrentDirectoryIsChanged(
dialog, '/My files/Downloads');
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
// Try to open a save dialog in Play Files. Since ARC is blocked by DLP, the
// dialog should open in the default root instead.
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'saveFile'}, 'android_files', [ENTRIES.hello], blockedCloser));
}
/**
* Tests the open file dialogs properly show DLP blocked files. If a file cannot
* be opened by the caller of the dialog, it should be marked as disabled in the
* details list. If such a file is selected, the "Open" dialog button should be
* disabled.
*/
export async function openDlpRestrictedFile() {
// Add entries to Downloads and setup the fake source URLs.
await addEntries(['local'], BASIC_LOCAL_ENTRY_SET);
await sendTestMessage({
name: 'setGetFilesSourcesMock',
fileNames: BASIC_LOCAL_ENTRY_SET.map(e => e.nameText),
sourceUrls: [
'https://blocked.com',
'https://allowed.com',
'https://blocked.com',
'https://warned.com',
'https://not-set.com',
],
});
// Setup the restrictions.
await sendTestMessage({name: 'setIsRestrictedByAnyRuleRestrictions'});
await sendTestMessage({name: 'setIsRestrictedDestinationRestriction'});
const closer = async (dialog: string) => {
// Wait for the file list to appear.
await remoteCall.waitForElement(dialog, '#file-list');
// Wait for the DLP managed icon to be shown - this means that metadata has
// been fetched, including the disabled status. Three are managed, but only
// two disabled.
await remoteCall.waitForElementsCount(
dialog, ['#file-list .dlp-managed-icon.is-dlp-restricted'], 3);
await remoteCall.waitForElementsCount(
dialog, ['#file-list .file[disabled]'], 2);
// Verify that the button is enabled when a non-blocked (warning level) file
// is selected.
await remoteCall.waitUntilSelected(dialog, ENTRIES.beautiful.nameText);
await remoteCall.waitForElement(dialog, okButton);
// Verify that the button is disabled when a blocked file is selected.
await remoteCall.waitUntilSelected(dialog, ENTRIES.hello.nameText);
await remoteCall.waitForElement(dialog, disabledOkButton);
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'openFile'}, 'downloads', BASIC_LOCAL_ENTRY_SET, closer));
}
/**
* Tests that the file picker disables DLP blocked files and doesn't allow
* opening them, while it allows selecting and opening folders.
*/
export async function openFolderDlpRestricted() {
// Make sure the file picker will open to Downloads.
sendBrowserTestCommand({name: 'setLastDownloadDir'});
const directoryAjpeg = new TestEntryInfo({
type: EntryType.FILE,
targetPath: `${ENTRIES.directoryA.nameText}/deep.jpg`,
sourceFileName: 'small.jpg',
mimeType: 'image/jpeg',
lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
nameText: 'deep.jpg',
sizeText: '886 bytes',
typeText: 'JPEG image',
});
const entries = [ENTRIES.directoryA, directoryAjpeg];
// Add entries to Downloads and setup the fake source URLs.
await addEntries(['local'], entries);
await sendTestMessage({
name: 'setGetFilesSourcesMock',
fileNames: [directoryAjpeg.targetPath],
sourceUrls: [
'https://blocked.com',
],
});
// Setup the restrictions.
await sendTestMessage({name: 'setIsRestrictedByAnyRuleRestrictions'});
await sendTestMessage({name: 'setIsRestrictedDestinationRestriction'});
const closer = async (dialog: string) => {
// Wait for directoryA to appear.
await remoteCall.waitForElement(
dialog, `#file-list [file-name="${ENTRIES.directoryA.nameText}"]`);
chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
'fakeMouseDoubleClick', dialog,
[`#file-list [file-name="${ENTRIES.directoryA.nameText}"]`]));
// Wait for the image file to appear.
await remoteCall.waitForElement(
dialog, `#file-list [file-name="${directoryAjpeg.nameText}"]`);
// Verify that the DLP managed icon for the image file is shown.
await remoteCall.waitForElementsCount(
dialog, ['#file-list .dlp-managed-icon.is-dlp-restricted'], 1);
// Verify that the image file is disabled.
await remoteCall.waitForElementsCount(
dialog, ['#file-list .file[disabled]'], 1);
// Verify that the button is disabled when the image file is selected.
await remoteCall.waitUntilSelected(dialog, directoryAjpeg.nameText);
await remoteCall.waitForElement(dialog, disabledOkButton);
// Click the close button to dismiss the dialog.
await remoteCall.waitAndClickElement(dialog, [cancelButton]);
};
chrome.test.assertEq(
undefined,
await remoteCall.openAndWaitForClosingDialog(
{type: 'openFile'}, 'downloads', [ENTRIES.directoryA], closer));
// Open Files app on Downloads as a folder picker.
const dialog = await remoteCall.setupAndWaitUntilReady(
RootPath.DOWNLOADS, entries, [], {type: DialogType.SELECT_UPLOAD_FOLDER});
// Verify that directoryA is not disabled.
await remoteCall.waitForElementsCount(
dialog, ['#file-list .file[disabled]'], 0);
// Select directoryA with the dialog.
await remoteCall.waitAndClickElement(
dialog, `#file-list [file-name="${ENTRIES.directoryA.nameText}"]`);
// Verify that directoryA selection is enabled while it contains a blocked
// file.
await sendTestMessage({
name: 'expectFileTask',
fileNames: [ENTRIES.directoryA.targetPath],
openType: 'open',
});
await remoteCall.waitAndClickElement(dialog, okButton);
}
/**
* Tests that DLP disabled file tasks are shown as disabled in the menu.
*/
export async function fileTasksDlpRestricted() {
const entry = ENTRIES.hello;
// Open Files app.
const appId =
await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);
// Override file tasks so that some are DLP disabled.
const fakeTasks = [
new FakeTask(
true, {appId: 'dummyId1', taskType: 'file', actionId: 'open-with'},
'DummyTask1', false, true),
new FakeTask(
false, {appId: 'dummyId2', taskType: 'file', actionId: 'open-with'},
'DummyTask2', false, false),
new FakeTask(
false, {appId: 'dummyId3', taskType: 'file', actionId: 'open-with'},
'DummyTask3', false, true),
];
await remoteCall.callRemoteTestUtil('overrideTasks', appId, [fakeTasks]);
// Open the context menu.
await remoteCall.showContextMenuFor(appId, entry.nameText);
// Verify that the default task item is visible but disabled.
await remoteCall.waitForElement(
appId,
['#file-context-menu:not([hidden]) ' +
'[command="#default-task"][disabled]:not([hidden])']);
// Select the file.
await remoteCall.waitUntilSelected(appId, entry.nameText);
// Display the tasks menu.
await remoteCall.expandOpenDropdown(appId);
// Wait for the dropdown menu to show.
await remoteCall.waitForElement(
appId, '#tasks-menu:not([hidden]) cr-menu-item');
// Verify that the first and third tasks are disabled, and the second one is
// not.
await remoteCall.waitForElement(
appId, ['#tasks-menu:not([hidden]) cr-menu-item[disabled]:nth-child(1)']);
await remoteCall.waitForElement(
appId,
['#tasks-menu:not([hidden]) cr-menu-item:not([disabled]):nth-child(2)']);
await remoteCall.waitForElement(
appId, ['#tasks-menu:not([hidden]) cr-menu-item[disabled]:nth-child(3)']);
}
/**
* Tests that extraction works when the scoped file access delegate exists and
* correct output files are generated.
*/
export async function zipExtractRestrictedArchiveCheckContent() {
const entry = ENTRIES.zipArchive;
// Add entries to Downloads and setup the fake source URLs.
await addEntries(['local'], [entry]);
await sendTestMessage({
name: 'setGetFilesSourcesMock',
fileNames: [entry.nameText],
sourceUrls: ['https://blocked.com'],
});
// Setup the restrictions.
await sendTestMessage({name: 'setIsRestrictedByAnyRuleBlocked'});
// Setup the scoped file access delegate.
await sendTestMessage({name: 'setupScopedFileAccessDelegateAllowed'});
// Open Files app.
const appId =
await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);
// Wait for the DLP managed icon to be shown.
await remoteCall.waitForElementsCount(
appId, ['#file-list .dlp-managed-icon.is-dlp-restricted'], 1);
const targetDirectoryName = entry.nameText.split('.')[0];
// Expect newly extracted files to be added to the DLP daemon.
await sendTestMessage({
name: 'expectFilesAdditionToDaemon',
fileNames:
[targetDirectoryName + '/image.png', targetDirectoryName + '/text.txt'],
sourceUrls: ['https://blocked.com', 'https://blocked.com'],
});
// Select the file.
await remoteCall.waitUntilSelected(appId, entry.nameText);
// Right-click the selected file.
await remoteCall.waitAndRightClick(appId, '.table-row[selected]');
// Check: the context menu should appear.
await remoteCall.waitForElement(appId, '#file-context-menu:not([hidden])');
// Resolves when the new app window opens.
const waitForWindowPromise = remoteCall.waitForWindow();
// Click the 'Extract all' menu command.
await remoteCall.waitAndClickElement(
appId, '[command="#extract-all"]:not([hidden])');
const directoryQuery = '#file-list [file-name="' + targetDirectoryName + '"]';
// Check: the extract directory should appear.
await remoteCall.waitForElement(appId, directoryQuery);
// Check: The new window has navigated to the unzipped folder.
const newAppId = await waitForWindowPromise;
await remoteCall.waitUntilCurrentDirectoryIsChanged(
newAppId, '/My files/Downloads/' + targetDirectoryName);
// Double click the created directory to open it.
chrome.test.assertTrue(
!!await remoteCall.callRemoteTestUtil(
'fakeMouseDoubleClick', appId, [directoryQuery]),
'fakeMouseDoubleClick failed');
// Check: File content in the ZIP should appear.
await remoteCall.waitForFiles(
appId,
[
['folder', '--', 'Folder'],
['text.txt', '--', 'Plain text'],
['image.png', '--', 'PNG image'],
],
{ignoreFileSize: true, ignoreLastModifiedTime: true});
}
/**
* Tests that a copy or move IO task that completed with error due to block
* restriction properly updates the task state and shows a correct panel item.
*/
export async function blockShowsPanelItem() {
// Add entry to Downloads.
const entry = ENTRIES.hello;
await addEntries(['local'], [entry]);
// Open Files app.
const appId =
await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);
// Setup the restrictions.
await sendTestMessage({
name: 'setBlockedFilesTransfer',
fileNames: [entry.nameText],
});
// Mount a USB volume.
await sendTestMessage({name: 'mountFakeUsbEmpty'});
// Wait for the USB volume to mount.
const directoryTree = await DirectoryTreePageObject.create(appId);
await directoryTree.waitForItemByType('removable');
// Copy and paste the file to USB.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ true);
// Check that the error panel is open with correct primary and secondary text,
// and has the expected button types.
await verifyPanelItem(
appId, PanelType.ERROR, 'File blocked from copying',
`${entry.nameText} was blocked because of policy`,
StatusIndicator.FAILURE);
await verifyPanelButtonsAndClick(appId, 'dismiss', 'secondary');
// Cut and paste the file to USB.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ false);
// Check that the error panel is open with correct primary and secondary text,
// and has the expected button types.
await verifyPanelItem(
appId, PanelType.ERROR, 'File blocked from moving',
`${entry.nameText} was blocked because of policy`,
StatusIndicator.FAILURE);
await verifyPanelButtonsAndClick(appId, 'dismiss', 'primary');
}
/**
* Tests that a copy or move IO task that is paused due to warn restriction
* properly updates the task state and shows a correct panel item.
*/
export async function warnShowsPanelItem() {
// Add entry to Downloads.
const entry = ENTRIES.hello;
await addEntries(['local'], [entry]);
// Open Files app.
const appId =
await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);
// Set the mock to pause the first task.
await sendTestMessage({
name: 'setCheckFilesTransferMockToPause',
taskId: 1,
fileNames: [entry.nameText],
action: 'copy',
});
// Mount a USB volume.
await sendTestMessage({name: 'mountFakeUsbEmpty'});
// Wait for the USB volume to mount.
const directoryTree = await DirectoryTreePageObject.create(appId);
await directoryTree.waitForItemByType('removable');
// Copy and paste the file to USB.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ true);
// Check that the warning panel is open with correct primary and secondary
// text, and has the expected button types.
await verifyPanelItem(
appId, PanelType.INFO, 'Review is required before copying',
`${entry.nameText} may contain sensitive content`,
StatusIndicator.WARNING);
await verifyPanelButtonsAndClick(appId, 'cancel', 'secondary');
// Set the first mock to pause the task.
await sendTestMessage({
name: 'setCheckFilesTransferMockToPause',
taskId: 2,
fileNames: [entry.nameText],
action: 'move',
});
// Cut and paste the file to USB.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ false);
// Check that the warning panel is open with correct primary and secondary
// text, and has the expected button types.
await verifyPanelItem(
appId, PanelType.INFO, 'Review is required before moving',
`${entry.nameText} may contain sensitive content`,
StatusIndicator.WARNING);
await verifyPanelButtonsAndClick(appId, 'cancel', 'primary');
}
/**
* Test for http://b/299583281.
* Tests that after DLP warning times out, the copy or move IO task
* properly updates the task state and shows a correct panel item.
*/
export async function warnTimeoutShowsPanelItem() {
// Add entry to Downloads.
const entry = ENTRIES.hello;
await addEntries(['local'], [entry]);
// Open Files app.
const appId =
await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);
// Set the mock to pause the first task.
await sendTestMessage({
name: 'setCheckFilesTransferMockToPause',
taskId: 1,
fileNames: [entry.nameText],
action: 'copy',
});
// Mount a USB volume.
await sendTestMessage({name: 'mountFakeUsbEmpty'});
// Wait for the USB volume to mount.
const directoryTree = await DirectoryTreePageObject.create(appId);
await directoryTree.waitForItemByType('removable');
// Copy and paste the file to USB.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ true);
// Check that the warning panel is open with correct primary and secondary
// text, and has the expected button types.
await verifyPanelItem(
appId, PanelType.INFO, 'Review is required before copying',
`${entry.nameText} may contain sensitive content`,
StatusIndicator.WARNING);
// Fast forward to time out the warning.
await sendTestMessage({name: 'timeoutWarning'});
// Check that the warning panel is open with correct primary and secondary
// text, and has the expected button types.
await verifyPanelItem(
appId, PanelType.ERROR, 'Copying timed out',
'Try copying your files again', StatusIndicator.FAILURE);
await verifyPanelButtonsAndClick(appId, 'dismiss', 'secondary');
}
/**
* Tests that the summary panel shows the correct title when it contains a mix
* of warning (paused copy or move IO task) and error (blocked copy or move IO
* task) panels, or multiple warnings, but is not shown if only one panel is
* visible.
*/
export async function mixedSummaryDisplayPanel() {
// Add entry to Downloads.
const entry = ENTRIES.hello;
await addEntries(['local'], [entry]);
// Open Files app.
const appId =
await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);
// Block the second task.
await sendTestMessage({
name: 'setBlockedFilesTransfer',
fileNames: [entry.nameText],
});
// Mount a USB volume.
await sendTestMessage({name: 'mountFakeUsbEmpty'});
// Wait for the USB volume to mount.
const directoryTree = await DirectoryTreePageObject.create(appId);
await directoryTree.waitForItemByType('removable');
// Copy and paste the file to USB.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ true);
// Check that only 1 error panel is opened.
await remoteCall.waitForElementsCount(
appId, ['#progress-panel', `xf-panel-item`], 1);
await remoteCall.waitForElementsCount(
appId,
['#progress-panel', `xf-panel-item[panel-type="${PanelType.ERROR}"]`], 1);
// Set the mock to pause the second task.
await sendTestMessage({
name: 'setCheckFilesTransferMockToPause',
taskId: 2,
fileNames: [entry.nameText],
action: 'copy',
});
// Copy the file to USB.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ true);
// Check that the summary panel is open with correct title and the two sub
// panels (3 in total).
await remoteCall.waitForElementsCount(
appId, ['#progress-panel', 'xf-panel-item'], 3);
await verifyPanelItem(
appId, PanelType.SUMMARY, '1 error. 1 warning.', null,
StatusIndicator.FAILURE);
// Expand the summary panel if needed, in order to click on the individual
// ones.
await maybeExpandSummary(appId);
// Dismiss the error panel.
await remoteCall.waitAndClickElement(appId, [
'#progress-panel',
`xf-panel-item[panel-type="${PanelType.ERROR}"]`,
'xf-button#secondary-action',
]);
// Check that only 1 warning panel remains.
await remoteCall.waitForElementsCount(
appId, ['#progress-panel', `xf-panel-item`], 1);
await remoteCall.waitForElementsCount(
appId,
['#progress-panel', `xf-panel-item[panel-type="${PanelType.INFO}"]`], 1);
// Set the mock to pause the third task.
await sendTestMessage({
name: 'setCheckFilesTransferMockToPause',
taskId: 3,
fileNames: [entry.nameText],
action: 'copy',
});
// Copy the file to USB.
await copyOrMove(appId, entry, '/fake-usb', /*isCopy=*/ true);
// Check that the summary panel is open with correct title and the two sub
// panels (3 in total).
await remoteCall.waitForElementsCount(
appId, ['#progress-panel', 'xf-panel-item'], 3);
await remoteCall.waitForElementsCount(
appId,
['#progress-panel', `xf-panel-item[panel-type="${PanelType.INFO}"]`], 2);
await verifyPanelItem(
appId, PanelType.SUMMARY, '2 warnings.', null, StatusIndicator.WARNING);
}