diff --git a/.woodpecker.star b/.woodpecker.star index 030725f448..b74f9cd597 100644 --- a/.woodpecker.star +++ b/.woodpecker.star @@ -554,7 +554,7 @@ def e2eTests(ctx): for browser_name in browsers_for_suite: environment = { "HEADLESS": True, - "RETRY": "1", + "RETRY": "0", "REPORT_TRACING": params["reportTracing"], "OC_BASE_URL": "opencloud:9200", "OC_SHOW_USER_EMAIL_IN_RESULTS": True, diff --git a/tests/e2e/cucumber/features/search/search.feature b/tests/e2e/cucumber/features/search/search.feature index 687019c790..10f1d2daa8 100644 --- a/tests/e2e/cucumber/features/search/search.feature +++ b/tests/e2e/cucumber/features/search/search.feature @@ -56,22 +56,21 @@ Feature: Search | resource | | folder | | FolDer | - | PARENT | | new-lorem-big.txt | # subfolder search And "Alice" searches "child" using the global search and the "all files" filter Then following resources should be displayed in the search list for user "Alice" - | resource | - | child-one | - | child-two | + | resource | + | FolDer/child-one | + | FolDer/child-one/child-two | But following resources should not be displayed in the search list for user "Alice" - | resource | - | folder | - | FolDer | - | folder_from_brian | - | .hidden-file.txt | - | new-lorem-big.txt | + | resource | + | folder | + | FolDer | + | new_share_from_brian | + | .hidden-file.txt | + | new-lorem-big.txt | # received shares search And "Alice" searches "NEW" using the global search and the "all files" filter @@ -136,16 +135,16 @@ Feature: Search When "Alice" opens folder "mainFolder" And "Alice" searches "example" using the global search and the "all files" filter Then following resources should be displayed in the search list for user "Alice" - | resource | - | exampleInsideThePersonalSpace.txt | - | exampleInsideTheMainFolder.txt | - | exampleInsideTheSubFolder.txt | + | resource | + | exampleInsideThePersonalSpace.txt | + | mainFolder/exampleInsideTheMainFolder.txt | + | mainFolder/subFolder/exampleInsideTheSubFolder.txt | When "Alice" searches "example" using the global search and the "current folder" filter Then following resources should be displayed in the search list for user "Alice" - | resource | - | exampleInsideTheMainFolder.txt | - | exampleInsideTheSubFolder.txt | + | resource | + | mainFolder/exampleInsideTheMainFolder.txt | + | mainFolder/subFolder/exampleInsideTheSubFolder.txt | But following resources should not be displayed in the search list for user "Alice" | resource | | exampleInsideThePersonalSpace.txt | diff --git a/tests/e2e/cucumber/features/search/searchProjectSpace.feature b/tests/e2e/cucumber/features/search/searchProjectSpace.feature index 11d3aa2708..96c1ed799c 100644 --- a/tests/e2e/cucumber/features/search/searchProjectSpace.feature +++ b/tests/e2e/cucumber/features/search/searchProjectSpace.feature @@ -23,8 +23,8 @@ Feature: Search in the project space # search for project space objects When "Alice" searches "-'s" using the global search and the "all files" filter Then following resources should be displayed in the search list for user "Alice" - | resource | - | new-'single'quotes.txt | + | resource | + | folder(WithSymbols:!;_+-&)/new-'single'quotes.txt | But following resources should not be displayed in the search list for user "Alice" | resource | | folder(WithSymbols:!;_+-&) | @@ -33,6 +33,6 @@ Feature: Search in the project space | resource | | folder(WithSymbols:!;_+-&) | But following resources should not be displayed in the search list for user "Alice" - | resource | - | new-'single'quotes.txt | + | resource | + | folder(WithSymbols:!;_+-&)/new-'single'quotes.txt | And "Alice" logs out diff --git a/tests/e2e/cucumber/features/smoke/trashbinDelete.feature b/tests/e2e/cucumber/features/smoke/trashbinDelete.feature index 3f88d6bc42..88d9d5c9ad 100644 --- a/tests/e2e/cucumber/features/smoke/trashbinDelete.feature +++ b/tests/e2e/cucumber/features/smoke/trashbinDelete.feature @@ -47,17 +47,24 @@ Feature: Trashbin delete Given "Alice" creates the following folders in personal space using API | name | | folderToShare | - | empty-folder | + | empty-folder | And "Alice" creates the following files into personal space using API | pathToFile | content | | folderToShare/lorem.txt | lorem ipsum | | sample.txt | sample | + And "Alice" opens the "files" app + And following resources should be displayed in the files list for user "Alice" + | resource | + | sample.txt | And "Alice" shares the following resource using the sidebar panel | resource | recipient | type | role | resourceType | | folderToShare | Brian | user | Can edit | folder | And "Brian" logs in And "Brian" navigates to the shared with me page And "Brian" opens folder "folderToShare" + And following resources should be displayed in the files list for user "Brian" + | resource | + | lorem.txt | When "Brian" deletes the following resources using the sidebar panel | resource | | lorem.txt | diff --git a/tests/e2e/cucumber/steps/ui/resources.ts b/tests/e2e/cucumber/steps/ui/resources.ts index e71621679b..f9c578aaa2 100644 --- a/tests/e2e/cucumber/steps/ui/resources.ts +++ b/tests/e2e/cucumber/steps/ui/resources.ts @@ -6,7 +6,6 @@ import { expect } from '@playwright/test' import { config } from '../../../config' import { createResourceTypes, - displayedResourceType, shortcutType, ActionViaType, PanelType @@ -16,6 +15,7 @@ import { Resource } from '../../../support/objects/app-files' import * as runtimeFs from '../../../support/utils/runtimeFs' import { searchFilter } from '../../../support/objects/app-files/resource/actions' import { File } from '../../../support/types' +import { waitProcessingToFinish } from '../../../support/objects/app-files/fileEvents' When( '{string} creates the following resource(s)', @@ -365,21 +365,44 @@ When( ) Then( - /^following resources (should|should not) be displayed in the (search list|files list|Shares|trashbin) for user "([^"]*)"$/, + /^following resources (should|should not) be displayed in the (?:files list|Shares|trashbin) for user "([^"]*)"$/, + async function ( + this: World, + actionType: string, + stepUser: string, + stepTable: DataTable + ): Promise { + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + const resourceObject = new objects.applicationFiles.Resource({ page }) + for (const info of stepTable.hashes()) { + if (actionType === 'should') { + await expect(resourceObject.getResourceLocator(info.resource)).toBeVisible({ + timeout: config.timeout * 1000 + }) + await waitProcessingToFinish(page, info.resource) + return + } + await expect(resourceObject.getResourceLocator(info.resource)).not.toBeVisible() + } + } +) + +Then( + /^following resources (should|should not) be displayed in the search list for user "([^"]*)"$/, async function ( this: World, actionType: string, - listType: string, stepUser: string, stepTable: DataTable ): Promise { const { page } = this.actorsEnvironment.getActor({ key: stepUser }) const resourceObject = new objects.applicationFiles.Resource({ page }) - const actualList = await resourceObject.getDisplayedResources({ - keyword: listType as displayedResourceType - }) for (const info of stepTable.hashes()) { - expect(actualList.includes(info.resource)).toBe(actionType === 'should') + if (actionType === 'should') { + await expect(resourceObject.getResourceSearchItemLocator(info.resource)).toBeVisible() + return + } + await expect(resourceObject.getResourceSearchItemLocator(info.resource)).not.toBeVisible() } } ) diff --git a/tests/e2e/support/objects/app-files/fileEvents.ts b/tests/e2e/support/objects/app-files/fileEvents.ts new file mode 100644 index 0000000000..7503e2cc91 --- /dev/null +++ b/tests/e2e/support/objects/app-files/fileEvents.ts @@ -0,0 +1,68 @@ +import { Page, Locator } from '@playwright/test' +import util from 'util' +import { config } from '../../../config' + +const resourceNameSelector = + ':is(#files-files-table, .oc-tiles-item, #files-shared-with-me-accepted-section, .files-table) [data-test-resource-name="%s"]' +const resourceProcessingIcon = + '//*[@data-test-resource-name="%s"]/ancestor::*[self::li or self::tr]//span[@data-test-indicator-type="resource-processing"]' +const resourceLockIcon = + '//*[@data-test-resource-name="%s"]/ancestor::*[self::li or self::tr]//span[@data-test-indicator-type="resource-locked"]' + +const getResourceLocator = (page: Page, resource: string): Locator => { + return page.locator(util.format(resourceNameSelector, resource)) +} + +const getProcessingLocator = (page: Page, resource: string): Locator => { + return page.locator(util.format(resourceProcessingIcon, resource)) +} + +export const getLockLocator = (page: Page, resource: string): Locator => { + return page.locator(util.format(resourceLockIcon, resource)) +} + +export const waitProcessingToFinish = async (page: Page, resource: string): Promise => { + await waitNotToBeVisible( + page, + getProcessingLocator(page, resource), + 'Waiting for file processing to finish', + () => getResourceLocator(page, resource).waitFor() + ) +} + +export const waitForLockToDisappear = async (page: Page, resource: string): Promise => { + await waitNotToBeVisible( + page, + getLockLocator(page, resource), + 'Waiting for file lock to be removed', + () => getResourceLocator(page, resource).waitFor() + ) +} + +const waitNotToBeVisible = async ( + page: Page, + locator: Locator, + message: string, + fn?: () => Promise +) => { + const timeout = (config.timeout / 2) * 1000 + const startTime = Date.now() + let elapsedTime = 0 + + while (elapsedTime < timeout) { + if (!(await locator.isVisible())) { + return + } + console.info(`[INFO] ${message}...`) + await page.waitForTimeout(config.minTimeout * 1000) + await page.reload() + + if (fn) { + await fn() + } + + elapsedTime = Date.now() - startTime + } + + await Promise.reject(new Error(`${timeout}ms timeout exceeded:`)) +} diff --git a/tests/e2e/support/objects/app-files/index.ts b/tests/e2e/support/objects/app-files/index.ts index d55795899d..5fcac87cb5 100644 --- a/tests/e2e/support/objects/app-files/index.ts +++ b/tests/e2e/support/objects/app-files/index.ts @@ -5,3 +5,4 @@ export { Share } from './share' export { Spaces } from './spaces' export { Trashbin } from './trashbin' export { Search } from './search' +export * as fileEvents from './fileEvents' diff --git a/tests/e2e/support/objects/app-files/resource/actions.ts b/tests/e2e/support/objects/app-files/resource/actions.ts index 68d1d93757..5ac772a5de 100644 --- a/tests/e2e/support/objects/app-files/resource/actions.ts +++ b/tests/e2e/support/objects/app-files/resource/actions.ts @@ -7,7 +7,9 @@ import { editor, sidebar } from '../utils' import { environment, utils } from '../../../../support' import { config } from '../../../../config' import { File, Space } from '../../../types' +import { waitProcessingToFinish } from '../fileEvents' +const appLoadingSpinner = '#app-loading-spinner' const topbarFilenameSelector = '#app-top-bar-resource .oc-resource-name' const downloadFileButtonSingleShareView = '.oc-files-actions-download-file-trigger' const downloadFolderButtonSingleShareView = '.oc-files-actions-download-archive-trigger' @@ -41,9 +43,7 @@ const createNewOfficeDocumentFileBUtton = '//div[@id="new-file-menu-drop"]//span const createNewShortcutButton = '#new-shortcut-btn' const shortcutResorceInput = '#create-shortcut-modal-url-input' const saveTextFileInEditorButton = '#app-save-action:visible' -const textEditor = '#text-editor #text-editor-container' const textEditorPlainTextInput = '#text-editor #text-editor-container .cm-content' -const textEditorMarkdownInput = '#text-editor #text-editor-container .cm-content' const resourceNameInput = '.oc-modal input' const resourceUploadButton = '#upload-menu-btn' const fileUploadInput = '#files-file-upload-input' @@ -76,6 +76,7 @@ const searchList = '//div[@id="files-global-search-options"]//li[contains(@class,"preview")]//span[contains(@class,"oc-resource-name")]' const globalSearchOptions = '#files-global-search-options' const loadingSpinner = '#files-global-search-options .loading' +const searchListItem = '#files-global-search span[data-test-resource-name="%s"]' const filesViewOptionButton = '#files-view-options-btn' const hiddenFilesToggleButton = '//*[@data-testid="files-switch-hidden-files"]//button' const previewImage = '//main[@id="preview"]//div[contains(@class,"stage_media")]//img' @@ -128,6 +129,10 @@ const userAvatarInActivitypanelSelector = '[data-test-user-name="%s"]' const mobileViewmodeSwitchBtn = '#mobile-viewmode-switch-toggle' const mobileViewmodeSwitchDropdown = '#mobile-viewmode-switch-drop' +// file viewer +const pdfViewerContainer = '#pdf-viewer object.pdf-viewer' +const textEditorContainer = '#text-editor-container div.md-editor-content' + // online office locators // Collabora const collaboraDocPermissionModeSelector = '#permissionmode-container' @@ -152,6 +157,26 @@ const openWithButton = '//*[@id="oc-files-context-actions-context"]//span[text() const tilesSlider = '#tiles-size-slider' const undoBtn = 'action-handler' +export const getResourceLocator = ({ + page, + resource +}: { + page: Page + resource: string +}): Locator => { + return page.locator(util.format(resourceNameSelector, resource)) +} + +export const getResourceSearchItemLocator = ({ + page, + resource +}: { + page: Page + resource: string +}): Locator => { + return page.locator(util.format(searchListItem, resource)) +} + export const clickResource = async ({ page, path @@ -449,13 +474,11 @@ const createDocumentFile = async ( "Editor should be either 'Collabora' or 'OnlyOffice' but found " + editorToOpen ) } - const respPromise = Promise.all([ - page.waitForResponse((res) => res.status() === 207 && res.request().method() === 'PROPFIND') + await Promise.all([ + page.waitForResponse((res) => res.status() === 207 && res.request().method() === 'PROPFIND'), + editor.close(page) ]) - await editor.close(page) - await respPromise - await page.reload() await page.locator(util.format(resourceNameSelector, name)).waitFor() // wait for lock to be removed expect(getLockLocator({ page, resource: name })).not.toBeVisible() @@ -569,11 +592,7 @@ export const editTextDocument = async ({ name: string content: string }): Promise => { - const isMarkdownMode = await page.locator(textEditor).getAttribute('data-markdown-mode') - const inputLocator = - isMarkdownMode === 'true' ? textEditorMarkdownInput : textEditorPlainTextInput - - await page.locator(inputLocator).fill(content) + await page.locator(textEditorPlainTextInput).fill(content) await Promise.all([ page.waitForResponse((resp) => resp.status() === 204 && resp.request().method() === 'PUT'), page.waitForResponse((resp) => resp.status() === 207 && resp.request().method() === 'PROPFIND'), @@ -684,7 +703,7 @@ export const dropUploadFiles = async (args: uploadResourceArgs): Promise = const { page, resources } = args // waiting to files view - await page.locator(addNewResourceButton).waitFor() + await expect(page.locator(addNewResourceButton)).not.toHaveAttribute('disabled') await utils.dragDropFiles(page, resources, filesView) await page.locator(uploadInfoCloseButton).click() @@ -1561,6 +1580,7 @@ export const searchResourceGlobalSearch = async ( } await page.locator(globalSearchBarFilter).click() + await page.locator(appLoadingSpinner).waitFor({ state: 'detached' }) if (!keyword) { await page.locator(globalSearchInput).click() @@ -1788,6 +1808,8 @@ export interface openFileInViewerArgs { export const openFileInViewer = async (args: openFileInViewerArgs): Promise => { const { page, name, actionType } = args + await waitProcessingToFinish(page, name) + switch (actionType) { case 'OnlyOffice': await Promise.all([ @@ -1850,7 +1872,16 @@ export const openFileInViewer = async (args: openFileInViewerArgs): Promise resp.status() === 207 && resp.request().method() === 'PROPFIND' + ), + page.locator(util.format(resourceNameSelector, name)).click() + ]) + await page.locator(pdfViewerContainer).waitFor() + break + } case 'texteditor': { await Promise.all([ page.waitForResponse( @@ -1858,6 +1889,7 @@ export const openFileInViewer = async (args: openFileInViewerArgs): Promise): Promise { switch (args.keyword) { case 'files list': diff --git a/tests/e2e/support/objects/app-files/utils/sidebar.ts b/tests/e2e/support/objects/app-files/utils/sidebar.ts index c2b543ea8e..48b3e44b61 100644 --- a/tests/e2e/support/objects/app-files/utils/sidebar.ts +++ b/tests/e2e/support/objects/app-files/utils/sidebar.ts @@ -1,6 +1,7 @@ import { Page, expect } from '@playwright/test' import util from 'util' import { locatorUtils } from '../../../utils' +import { waitProcessingToFinish, waitForLockToDisappear } from '../fileEvents' const contextMenuSelector = ` //button[ @@ -52,6 +53,10 @@ export const open = async ({ page: Page resource?: string }): Promise => { + if (resource) { + await waitForLockToDisappear(page, resource) + await waitProcessingToFinish(page, resource) + } if (await page.locator('#app-sidebar').count()) { await Promise.all([ page.locator('#app-sidebar').waitFor({ state: 'detached' }),