Merge pull request #51208 from nextcloud/backport/51146/stable29

[stable29] fix(files_trashbin): disable bulk download for trashbin
This commit is contained in:
Andy Scherzinger 2025-03-19 22:21:18 +01:00 committed by GitHub
commit 53fbf44b60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 282 additions and 98 deletions

View file

@ -19,8 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Node, View } from '@nextcloud/files'
import { generateUrl } from '@nextcloud/router'
import { FileAction, Permission, Node, FileType, View, DefaultType } from '@nextcloud/files'
import { FileAction, Permission, FileType, DefaultType } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'
@ -110,7 +111,7 @@ export const action = new FileAction({
displayName: () => t('files', 'Download'),
iconSvgInline: () => ArrowDownSvg,
enabled(nodes: Node[]) {
enabled(nodes: Node[], view: View) {
if (nodes.length === 0) {
return false
}
@ -123,6 +124,11 @@ export const action = new FileAction({
return false
}
// Trashbin does not allow batch download
if (nodes.length > 1 && view.id === 'trashbin') {
return false
}
return nodes.every(isDownloadable)
},

View file

@ -26,6 +26,7 @@
<NcCheckboxRadioSwitch v-else
:aria-label="ariaLabel"
:checked="isSelected"
data-cy-files-list-row-checkbox
@update:checked="onSelectionChange" />
</td>
</template>

View file

@ -23,7 +23,12 @@
<tr class="files-list__row-head">
<th class="files-list__column files-list__row-checkbox"
@keyup.esc.exact="resetSelection">
<NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
<NcCheckboxRadioSwitch data-cy-files-list-selection-checkbox
:aria-label="checkboxTitle"
:checked="isAllSelected"
:indeterminate="isSomeSelected"
:title="checkboxTitle"
@update:checked="onToggleAll" />
</th>
<!-- Columns display -->
@ -127,6 +132,7 @@ export default defineComponent({
filesStore,
selectionStore,
checkboxTitle: t('files', 'Toggle selection for all files and folders'),
currentView,
}
},
@ -140,21 +146,6 @@ export default defineComponent({
return this.currentView?.columns || []
},
dir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
},
selectAllBind() {
const label = t('files', 'Toggle selection for all files and folders')
return {
'aria-label': label,
checked: this.isAllSelected,
indeterminate: this.isSomeSelected,
title: label,
}
},
selectedNodes() {
return this.selectionStore.selected
},
@ -173,11 +164,11 @@ export default defineComponent({
},
methods: {
ariaSortForMode(mode: string): ARIAMixin['ariaSort'] {
ariaSortForMode(mode: string): 'ascending' | 'descending' | undefined {
if (this.sortingMode === mode) {
return this.isAscSorting ? 'ascending' : 'descending'
}
return null
return undefined
},
classForColumn(column) {

View file

@ -20,6 +20,19 @@
*
*/
import type { User } from '@nextcloud/cypress'
export const selectAllFiles = () => {
cy.get('[data-cy-files-list-selection-checkbox]')
.findByRole('checkbox', { checked: false })
.click({ force: true })
}
export const deselectAllFiles = () => {
cy.get('[data-cy-files-list-selection-checkbox]')
.findByRole('checkbox', { checked: true })
.click({ force: true })
}
export const getRowForFileId = (fileid: number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`)
@ -44,27 +57,37 @@ const searchForActionInRow = (row: JQuery<HTMLElement>, actionId: string): Cypre
export const getActionEntryForFileId = (fileid: number, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
// If we cannot find the action in the row, it might be in the action menu
return getRowForFileId(fileid).should('be.visible')
.then(row => searchForActionInRow(row, actionId))
.then((row) => searchForActionInRow(row, actionId))
}
export const getActionEntryForFile = (filename: string, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
// If we cannot find the action in the row, it might be in the action menu
return getRowForFile(filename).should('be.visible')
.then(row => searchForActionInRow(row, actionId))
.then((row) => searchForActionInRow(row, actionId))
}
export const triggerActionForFileId = (fileid: number, actionId: string) => {
// Even if it's inline, we open the action menu to get all actions visible
getActionButtonForFileId(fileid).click({ force: true })
// wait for the actions menu to be visible
cy.findByRole('menu').findAllByRole('menuitem').first().should('be.visible')
getActionEntryForFileId(fileid, actionId)
.find('button').last()
.should('exist').click({ force: true })
.find('button').last().as('actionButton')
.scrollIntoView()
cy.get('@actionButton')
.should('be.visible')
.click({ force: true })
}
export const triggerActionForFile = (filename: string, actionId: string) => {
// Even if it's inline, we open the action menu to get all actions visible
getActionButtonForFile(filename).click({ force: true })
// wait for the actions menu to be visible
cy.findByRole('menu').findAllByRole('menuitem').first().should('be.visible')
getActionEntryForFile(filename, actionId)
.find('button').last()
.should('exist').click({ force: true })
.find('button').last().as('actionButton')
.scrollIntoView()
cy.get('@actionButton')
.should('be.visible')
.click({ force: true })
}
export const triggerInlineActionForFileId = (fileid: number, actionId: string) => {
@ -181,3 +204,93 @@ export const clickOnBreadcrumbs = (label: string) => {
cy.get('[data-cy-files-content-breadcrumbs]').contains(label).click()
cy.wait('@propfind')
}
/**
* Check validity of an input element
* @param validity The expected validity message (empty string means it is valid)
* @example
* ```js
* cy.findByRole('textbox')
* .should(haveValidity(/must not be empty/i))
* ```
*/
export const haveValidity = (validity: string | RegExp) => {
if (typeof validity === 'string') {
return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.equal(validity)
}
return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.match(validity)
}
export const deleteFileWithRequest = (user: User, path: string) => {
// Ensure path starts with a slash and has no double slashes
path = `/${path}`.replace(/\/+/g, '/')
cy.request('/csrftoken').then(({ body }) => {
const requestToken = body.token
cy.request({
method: 'DELETE',
url: `${Cypress.env('baseUrl')}/remote.php/dav/files/${user.userId}${path}`,
auth: {
user: user.userId,
password: user.password,
},
headers: {
requestToken,
},
retryOnStatusCodeFailure: true,
})
})
}
export const triggerFileListAction = (actionId: string) => {
cy.get(`button[data-cy-files-list-action="${CSS.escape(actionId)}"]`).last()
.should('exist').click({ force: true })
}
export const reloadCurrentFolder = () => {
cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind')
cy.get('[data-cy-files-content-breadcrumbs]').findByRole('button', { description: 'Reload current directory' }).click()
cy.wait('@propfind')
}
/**
* Enable the grid mode for the files list.
* Will fail if already enabled!
*/
export function enableGridMode() {
cy.intercept('**/apps/files/api/v1/config/grid_view').as('setGridMode')
cy.findByRole('button', { name: 'Switch to grid view' })
.should('be.visible')
.click()
cy.wait('@setGridMode')
}
/**
* Calculate the needed viewport height to limit the visible rows of the file list.
* Requires a logged in user.
*
* @param rows The number of rows that should be displayed at the same time
*/
export function calculateViewportHeight(rows: number): Cypress.Chainable<number> {
cy.visit('/apps/files')
return cy.get('[data-cy-files-list]')
.should('be.visible')
.then((filesList) => {
const windowHeight = Cypress.$('body').outerHeight()!
// Size of other page elements
const outerHeight = Math.ceil(windowHeight - filesList.outerHeight()!)
// Size of before and filters
const beforeHeight = Math.ceil(Cypress.$('.files-list__before').outerHeight()!)
const filterHeight = Math.ceil(Cypress.$('.files-list__filters').outerHeight()!)
// Size of the table header
const tableHeaderHeight = Math.ceil(Cypress.$('[data-cy-files-list-thead]').outerHeight()!)
// table row height
const rowHeight = Math.ceil(Cypress.$('[data-cy-files-list-tbody] tr').outerHeight()!)
// sum it up
const viewportHeight = outerHeight + beforeHeight + filterHeight + tableHeaderHeight + rows * rowHeight
cy.log(`Calculated viewport height: ${viewportHeight} (${outerHeight} + ${beforeHeight} + ${filterHeight} + ${tableHeaderHeight} + ${rows} * ${rowHeight})`)
return cy.wrap(viewportHeight)
})
}

View file

@ -17,13 +17,18 @@ const haveValidity = (validity: string | RegExp) => {
describe('files: Rename nodes', { testIsolation: true }, () => {
let user: User
beforeEach(() => cy.createRandomUser().then(($user) => {
user = $user
beforeEach(() => {
cy.createRandomUser().then(($user) => {
user = $user
cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
cy.login(user)
// create a file called "file.txt"
cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
// login and visit files app
cy.login(user)
})
cy.visit('/apps/files')
}))
})
it('can rename a file', () => {
// All are visible by default

View file

@ -0,0 +1,70 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
// @ts-expect-error package has wrong typings
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
import { deleteFileWithRequest, getRowForFileId, selectAllFiles, triggerActionForFileId } from '../files/FilesUtils.ts'
describe('files_trashbin: download files', { testIsolation: true }, () => {
let user: User
const fileids: number[] = []
deleteDownloadsFolderBeforeEach()
before(() => {
cy.createRandomUser().then(($user) => {
user = $user
cy.uploadContent(user, new Blob(['<content>']), 'text/plain', '/file.txt')
.then(({ headers }) => fileids.push(Number.parseInt(headers['oc-fileid'])))
.then(() => deleteFileWithRequest(user, '/file.txt'))
cy.uploadContent(user, new Blob(['<content>']), 'text/plain', '/other-file.txt')
.then(({ headers }) => fileids.push(Number.parseInt(headers['oc-fileid'])))
.then(() => deleteFileWithRequest(user, '/other-file.txt'))
})
})
beforeEach(() => {
cy.login(user)
cy.visit('/apps/files/trashbin')
})
it('can download file', () => {
getRowForFileId(fileids[0]).should('be.visible')
getRowForFileId(fileids[1]).should('be.visible')
triggerActionForFileId(fileids[0], 'download')
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 8)
.and('equal', '<content>')
})
it('can download a file using default action', () => {
getRowForFileId(fileids[0])
.should('be.visible')
.findByRole('button', { name: 'Download' })
.click({ force: true })
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 8)
.and('equal', '<content>')
})
// TODO: Fix this as this dependens on the webdav zip folder plugin not working for trashbin (and never worked with old NC legacy download ajax as well)
it('does not offer bulk download', () => {
cy.get('[data-cy-files-list-row-checkbox]').should('have.length', 2)
selectAllFiles()
cy.get('.files-list__selected').should('have.text', '2 selected')
cy.get('[data-cy-files-list-selection-action="restore"]').should('be.visible')
cy.get('[data-cy-files-list-selection-action="download"]').should('not.exist')
})
})

4
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

104
package-lock.json generated
View file

@ -148,7 +148,7 @@
"karma-spec-reporter": "^0.0.36",
"karma-viewport": "^1.0.9",
"mime": "^4.0.6",
"puppeteer": "^22.5.0",
"puppeteer": "^24.4.0",
"raw-loader": "^4.0.2",
"regextras": "^0.8.0",
"sass": "^1.66.1",
@ -5569,19 +5569,18 @@
"license": "BSD-3-Clause"
},
"node_modules/@puppeteer/browsers": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz",
"integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==",
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.8.0.tgz",
"integrity": "sha512-yTwt2KWRmCQAfhvbCRjebaSX8pV1//I0Y3g+A7f/eS7gf0l4eRJoUCvcYdVtboeU4CTOZQuqYbZNS8aBYb8ROQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.3.5",
"debug": "^4.4.0",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.4.0",
"semver": "^7.6.3",
"tar-fs": "^3.0.6",
"unbzip2-stream": "^1.4.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.1",
"tar-fs": "^3.0.8",
"yargs": "^17.7.2"
},
"bin": {
@ -8875,9 +8874,9 @@
}
},
"node_modules/bare-os": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.5.1.tgz",
"integrity": "sha512-LvfVNDcWLw2AnIw5f2mWUgumW3I3N/WYGiWeimhQC1Ybt71n2FjlS9GJKeCnFeg1MKZHxzIFmpFnBXDI+sBeFg==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.0.tgz",
"integrity": "sha512-BUrFS5TqSBdA0LwHop4OjPJwisqxGy6JsWVqV6qaFoe965qqtaKfDzHY5T2YA1gUL0ZeeQeA+4BBc1FJTcHiPw==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
@ -9789,15 +9788,14 @@
}
},
"node_modules/chromium-bidi": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz",
"integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-2.1.2.tgz",
"integrity": "sha512-vtRWBK2uImo5/W2oG6/cDkkHSm+2t6VHgnj+Rcwhb0pP74OoUb4GipyRX/T/y39gYQPhioP0DPShn+A7P6CHNw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"mitt": "3.0.1",
"urlpattern-polyfill": "10.0.0",
"zod": "3.23.8"
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"peerDependencies": {
"devtools-protocol": "*"
@ -11764,9 +11762,9 @@
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1312386",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
"integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==",
"version": "0.0.1413902",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1413902.tgz",
"integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
"dev": true,
"license": "BSD-3-Clause"
},
@ -23432,37 +23430,40 @@
}
},
"node_modules/puppeteer": {
"version": "22.15.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.15.0.tgz",
"integrity": "sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==",
"version": "24.4.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.4.0.tgz",
"integrity": "sha512-E4JhJzjS8AAI+6N/b+Utwarhz6zWl3+MR725fal+s3UlOlX2eWdsvYYU+Q5bXMjs9eZEGkNQroLkn7j11s2k1Q==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.3.0",
"@puppeteer/browsers": "2.8.0",
"chromium-bidi": "2.1.2",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1312386",
"puppeteer-core": "22.15.0"
"devtools-protocol": "0.0.1413902",
"puppeteer-core": "24.4.0",
"typed-query-selector": "^2.12.0"
},
"bin": {
"puppeteer": "lib/esm/puppeteer/node/cli.js"
"puppeteer": "lib/cjs/puppeteer/node/cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/puppeteer-core": {
"version": "22.15.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz",
"integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==",
"version": "24.4.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.4.0.tgz",
"integrity": "sha512-eFw66gCnWo0X8Hyf9KxxJtms7a61NJVMiSaWfItsFPzFBsjsWdmcNlBdsA1WVwln6neoHhsG+uTVesKmTREn/g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.3.0",
"chromium-bidi": "0.6.3",
"debug": "^4.3.6",
"devtools-protocol": "0.0.1312386",
"ws": "^8.18.0"
"@puppeteer/browsers": "2.8.0",
"chromium-bidi": "2.1.2",
"debug": "^4.4.0",
"devtools-protocol": "0.0.1413902",
"typed-query-selector": "^2.12.0",
"ws": "^8.18.1"
},
"engines": {
"node": ">=18"
@ -28295,6 +28296,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typed-query-selector": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
@ -28394,17 +28402,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.2.1",
"through": "^2.3.8"
}
},
"node_modules/underscore": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
@ -28743,13 +28740,6 @@
"integrity": "sha512-FOEojW4XReTmtZOB7xqSHmJZhrNTmClhBriwLTmle4iA7bwuCo6ldSfbtsFSb8bTf3E0a3XpfonAdaur9vqq8A==",
"license": "MIT"
},
"node_modules/urlpattern-polyfill": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
"dev": true,
"license": "MIT"
},
"node_modules/utif": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz",
@ -30592,9 +30582,9 @@
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"dev": true,
"license": "MIT",
"funding": {

View file

@ -175,7 +175,7 @@
"karma-spec-reporter": "^0.0.36",
"karma-viewport": "^1.0.9",
"mime": "^4.0.6",
"puppeteer": "^22.5.0",
"puppeteer": "^24.4.0",
"raw-loader": "^4.0.2",
"regextras": "^0.8.0",
"sass": "^1.66.1",

View file

@ -33,8 +33,11 @@
* preprocessor, which is needed to be able to debug tests properly in a browser.
*/
const { existsSync } = require('node:fs');
if (!process.env.CHROMIUM_BIN) {
process.env.CHROMIUM_BIN = require('puppeteer').executablePath()
const chrome = require('puppeteer').executablePath()
process.env.CHROMIUM_BIN = chrome
}
const webpack = require('webpack');
@ -273,14 +276,19 @@ module.exports = function(config) {
// - PhantomJS
// - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
// use PhantomJS_debug for extra local debug
browsers: ['ChromiumHeadless'],
browsers: ['Chrome_without_sandbox'],
// you can define custom flags
customLaunchers: {
PhantomJS_debug: {
base: 'PhantomJS',
debug: true
}
},
// fix CI
Chrome_without_sandbox: {
base: 'ChromiumHeadless',
flags: ['--no-sandbox'],
},
},
// If browser does not capture in given timeout [ms], kill it