From 971cce6a4758454f29c3daf6551a72ff1bd8a787 Mon Sep 17 00:00:00 2001 From: Ashim Shrestha Date: Wed, 20 Aug 2025 14:56:53 +0545 Subject: [PATCH] test: replace cucumber with playwright for internalLink.feature Signed-off-by: Ashim Shrestha --- .drone.star | 75 +++++++++++- package.json | 1 + .../helpers/setAccessAndRefreshToken.ts | 10 ++ tests/e2e-playwright/playwright.config.ts | 46 +++++++ .../e2e-playwright/specs/internalLink.spec.ts | 97 +++++++++++++++ tests/e2e-playwright/steps/api/api.ts | 91 ++++++++++++++ tests/e2e-playwright/steps/ui/index.ts | 4 + tests/e2e-playwright/steps/ui/public.ts | 23 ++++ tests/e2e-playwright/steps/ui/resources.ts | 44 +++++++ tests/e2e-playwright/steps/ui/session.ts | 73 +++++++++++ tests/e2e-playwright/steps/ui/shares.ts | 114 ++++++++++++++++++ tests/e2e/support/api/davSpaces/spaces.ts | 2 +- 12 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 tests/e2e-playwright/helpers/setAccessAndRefreshToken.ts create mode 100644 tests/e2e-playwright/playwright.config.ts create mode 100644 tests/e2e-playwright/specs/internalLink.spec.ts create mode 100644 tests/e2e-playwright/steps/api/api.ts create mode 100644 tests/e2e-playwright/steps/ui/index.ts create mode 100644 tests/e2e-playwright/steps/ui/public.ts create mode 100644 tests/e2e-playwright/steps/ui/resources.ts create mode 100644 tests/e2e-playwright/steps/ui/session.ts create mode 100644 tests/e2e-playwright/steps/ui/shares.ts diff --git a/.drone.star b/.drone.star index 9e60fd55b98..8038662b4a0 100644 --- a/.drone.star +++ b/.drone.star @@ -223,9 +223,10 @@ def stagePipelines(ctx): if (determineReleasePackage(ctx) != None): return unit_test_pipelines + e2e_playwright_pipeline = e2eTestsOnPlaywright(ctx) e2e_pipelines = e2eTests(ctx) keycloak_pipelines = e2eTestsOnKeycloak(ctx) - return unit_test_pipelines + pipelinesDependsOn(e2e_pipelines + keycloak_pipelines, unit_test_pipelines) + return unit_test_pipelines + pipelinesDependsOn(e2e_playwright_pipeline + e2e_pipelines + keycloak_pipelines, unit_test_pipelines) def afterPipelines(ctx): return build(ctx) + pipelinesDependsOn(notify(ctx), build(ctx)) @@ -535,6 +536,64 @@ def unitTests(ctx): }, }] +def e2eTestsOnPlaywright(ctx): + e2e_workspace = { + "base": dir["base"], + "path": config["app"], + } + + e2e_trigger = { + "ref": [ + "refs/heads/master", + "refs/heads/stable-*", + "refs/tags/**", + "refs/pull/**", + ], + } + + pipelines = [] + + # pipeline steps + steps = skipIfUnchanged(ctx, "e2e-tests-playwright") + + environment = { + "BASE_URL_OCIS": "ocis:9200", + "PLAYWRIGHT_BROWSERS_PATH": ".playwright", + } + + steps += restoreBuildArtifactCache(ctx, "pnpm", ".pnpm-store") + \ + installPnpm() + \ + restoreBrowsersCache() + \ + restoreBuildArtifactCache(ctx, "web-dist", "dist") + + if ctx.build.event == "cron": + steps += restoreBuildArtifactCache(ctx, "ocis", "ocis") + else: + steps += restoreOcisCache() + + steps += ocisService() + + steps += [{ + "name": "e2e-tests-playwright", + "image": OC_CI_NODEJS_IMAGE, + "environment": environment, + "commands": [ + "pnpm test:e2e:playwright --project=chromium", + ], + }] + + pipelines.append({ + "kind": "pipeline", + "type": "docker", + "name": "e2e-tests-playwright", + "workspace": e2e_workspace, + "steps": steps, + "depends_on": ["cache-ocis"], + "trigger": e2e_trigger, + }) + + return pipelines + def e2eTests(ctx): e2e_workspace = { "base": dir["base"], @@ -1365,12 +1424,26 @@ def skipIfUnchanged(ctx, type): } return [skip_step] + if type == "e2e-tests-playwright": + e2e_playwright_skip_steps = [ + "^__fixtures__/.*", + "^__mocks__/.*", + "^packages/.*/tests/.*", + "^tests/unit/.*", + "^tests/e2e/cucumber/.*", + ] + skip_step["settings"] = { + "ALLOW_SKIP_CHANGED": base_skip_steps + e2e_playwright_skip_steps, + } + return [skip_step] + if type == "e2e-tests": e2e_skip_steps = [ "^__fixtures__/.*", "^__mocks__/.*", "^packages/.*/tests/.*", "^tests/unit/.*", + "^tests/e2e-playwright/.*", ] skip_step["settings"] = { "ALLOW_SKIP_CHANGED": base_skip_steps + e2e_skip_steps, diff --git a/package.json b/package.json index 4109c9fd328..052c416d177 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "eslint vite.config.ts '{packages,tests}/**/*.{js,ts,vue}' --color", "serve": "SERVER=true pnpm build:w", "test:e2e:cucumber": "NODE_TLS_REJECT_UNAUTHORIZED=0 TS_NODE_PROJECT=./tests/e2e/cucumber/tsconfig.json cucumber-js --profile=e2e -f json:tests/e2e/cucumber/report/cucumber_report.json", + "test:e2e:playwright": "NODE_TLS_REJECT_UNAUTHORIZED=0 npx playwright test --config=tests/e2e-playwright/", "test:unit": "NODE_OPTIONS=--unhandled-rejections=throw vitest", "licenses:check": "license-checker-rseidelsohn --summary --relativeLicensePath --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL;BlueOak-1.0.0' --excludePackages '@ownclouders/babel-preset;@ownclouders/eslint-config;@ownclouders/prettier-config;@ownclouders/tsconfig;@ownclouders/web-client;@ownclouders/web-pkg;external;web-app-files;text-editor;preview;web-app-ocm;@ownclouders/design-system;pdf-viewer;web-app-search;admin-settings;webfinger;web-runtime;@ownclouders/web-test-helpers'", "licenses:csv": "license-checker-rseidelsohn --relativeLicensePath --csv --out ./third-party-licenses/third-party-licenses.csv", diff --git a/tests/e2e-playwright/helpers/setAccessAndRefreshToken.ts b/tests/e2e-playwright/helpers/setAccessAndRefreshToken.ts new file mode 100644 index 00000000000..d19225832c4 --- /dev/null +++ b/tests/e2e-playwright/helpers/setAccessAndRefreshToken.ts @@ -0,0 +1,10 @@ +import { config } from '../../e2e/config.js' +import { api } from '../../e2e/support' +import { UsersEnvironment } from '../../e2e/support/environment' + +export async function setAccessAndRefreshToken(usersEnvironment: UsersEnvironment) { + if (!config.basicAuth && !config.predefinedUsers) { + let user = usersEnvironment.getUser({ key: config.adminUsername }) + await api.token.setAccessAndRefreshToken(user) + } +} diff --git a/tests/e2e-playwright/playwright.config.ts b/tests/e2e-playwright/playwright.config.ts new file mode 100644 index 00000000000..028f82f95f2 --- /dev/null +++ b/tests/e2e-playwright/playwright.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + // Look for test files in the following directory, relative to this configuration file. + testDir: 'specs', + + // Run all tests in parallel. + fullyParallel: false, + + // Fail the build on CI if you accidentally left test.only in the source code. + forbidOnly: !!process.env.CI, + + // Retry on CI only. + retries: process.env.CI ? 1 : 0, + + // Opt out of parallel tests on CI. + workers: process.env.CI ? 1 : undefined, + + // Reporter to use + reporter: 'html', + + use: { + ignoreHTTPSErrors: true, + + // Collect trace when retrying the failed test. + trace: 'on-first-retry' + }, + // Configure projects for major browsers. + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] } + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] } + } + ] +}) diff --git a/tests/e2e-playwright/specs/internalLink.spec.ts b/tests/e2e-playwright/specs/internalLink.spec.ts new file mode 100644 index 00000000000..9bc906d08f0 --- /dev/null +++ b/tests/e2e-playwright/specs/internalLink.spec.ts @@ -0,0 +1,97 @@ +import { expect, test } from '@playwright/test' +import { config } from '../../e2e/config.js' +import { + ActorsEnvironment, + UsersEnvironment, + LinksEnvironment, + FilesEnvironment +} from '../../e2e/support/environment' +import { setAccessAndRefreshToken } from '../helpers/setAccessAndRefreshToken' +import * as api from '../steps/api/api' +import * as ui from '../steps/ui/index' + +test.describe('internal link share', () => { + let actorsEnvironment + const usersEnvironment = new UsersEnvironment() + const linksEnvironment = new LinksEnvironment() + const filesEnvironment = new FilesEnvironment() + + test.beforeEach(async ({ browser }) => { + actorsEnvironment = new ActorsEnvironment({ + context: { + acceptDownloads: config.acceptDownloads, + reportDir: config.reportDir, + tracingReportDir: config.tracingReportDir, + reportHar: config.reportHar, + reportTracing: config.reportTracing, + reportVideo: config.reportVideo, + failOnUncaughtConsoleError: config.failOnUncaughtConsoleError + }, + browser: browser + }) + + await setAccessAndRefreshToken(usersEnvironment) + + await api.userHasBeenCreated({ usersEnvironment, stepUser: 'Admin', userToBeCreated: 'Alice' }) + await api.userHasBeenCreated({ usersEnvironment, stepUser: 'Admin', userToBeCreated: 'Brian' }) + + await ui.logInUser({ usersEnvironment, actorsEnvironment, stepUser: 'Alice' }) + await ui.logInUser({ usersEnvironment, actorsEnvironment, stepUser: 'Brian' }) + + await api.userHasCreatedFolder({ usersEnvironment, stepUser: 'Alice', folderName: 'myfolder' }) + + await api.userHasSharedResource({ + usersEnvironment, + stepUser: 'Alice', + resource: 'myfolder', + recipient: 'Brian', + type: 'user', + role: 'Can edit', + resourceType: 'folder' + }) + + await api.userHasCreatedPublicLinkOfResource({ + usersEnvironment, + stepUser: 'Alice', + resource: 'myfolder', + role: 'Invited people' + }) + }) + + test('opening a link with internal role', async () => { + await ui.openPublicLink({ + actorsEnvironment, + linksEnvironment, + stepUser: 'Brian', + name: 'Unnamed link' + }) + await ui.navigateToSharedWithMePage({ actorsEnvironment, stepUser: 'Brian' }) + await ui.uploadResource({ + actorsEnvironment, + filesEnvironment, + stepUser: 'Brian', + resource: 'simple.pdf', + to: 'myfolder' + }) + await ui.updateShareeRole({ + usersEnvironment, + actorsEnvironment, + stepUser: 'Alice', + resource: 'myfolder', + recipient: 'Brian', + type: 'user', + role: 'Can view', + resourceType: 'folder' + }) + await ui.logOutUser({ actorsEnvironment, stepUser: 'Alice' }) + + expect( + await ui.isAbleToEditFileOrFolder({ + actorsEnvironment, + stepUser: 'Brian', + resource: 'myfolder' + }) + ).toBeFalsy() + await ui.logOutUser({ actorsEnvironment, stepUser: 'Brian' }) + }) +}) diff --git a/tests/e2e-playwright/steps/api/api.ts b/tests/e2e-playwright/steps/api/api.ts new file mode 100644 index 00000000000..4fcb64f62f2 --- /dev/null +++ b/tests/e2e-playwright/steps/api/api.ts @@ -0,0 +1,91 @@ +import { config } from '../../../e2e/config.js' +import { UsersEnvironment } from '../../../e2e/support/environment' +import { api } from '../../../e2e/support' +import { ResourceType } from '../../../e2e/support/api/share/share' + +export async function userHasBeenCreated({ + usersEnvironment, + stepUser, + userToBeCreated +}: { + usersEnvironment: UsersEnvironment + stepUser: string + userToBeCreated: string +}): Promise { + const admin = usersEnvironment.getUser({ key: stepUser }) + const user = usersEnvironment.getUser({ key: userToBeCreated }) + // do not try to create users when using predefined users + if (!config.predefinedUsers) { + await api.provision.createUser({ user, admin }) + } +} + +export async function userHasCreatedFolder({ + usersEnvironment, + stepUser, + folderName +}: { + usersEnvironment: UsersEnvironment + stepUser: string + folderName: string +}): Promise { + const user = usersEnvironment.getUser({ key: stepUser }) + await api.dav.createFolderInsidePersonalSpace({ user, folder: folderName }) +} + +export async function userHasSharedResource({ + usersEnvironment, + stepUser, + resource, + recipient, + type, + role, + resourceType +}: { + usersEnvironment: UsersEnvironment + stepUser: string + resource: string + recipient: string + type: string + role: string + resourceType: ResourceType +}): Promise { + const user = usersEnvironment.getUser({ key: stepUser }) + await api.share.createShare({ + user, + path: resource, + shareType: type, + shareWith: recipient, + role: role, + resourceType: resourceType as ResourceType + }) +} + +export async function userHasCreatedPublicLinkOfResource({ + usersEnvironment, + stepUser, + resource, + role, + name, + password, + space +}: { + usersEnvironment: UsersEnvironment + stepUser: string + resource: string + role: string + name?: string + password?: undefined + space?: 'Personal' +}) { + const user = usersEnvironment.getUser({ key: stepUser }) + + await api.share.createLinkShare({ + user, + path: resource, + password: password, + name: name ? name : 'Unnamed link', + role: role, + spaceName: space + }) +} diff --git a/tests/e2e-playwright/steps/ui/index.ts b/tests/e2e-playwright/steps/ui/index.ts new file mode 100644 index 00000000000..f87ac11bfdd --- /dev/null +++ b/tests/e2e-playwright/steps/ui/index.ts @@ -0,0 +1,4 @@ +export * from './public' +export * from './shares' +export * from './resources' +export * from './session' diff --git a/tests/e2e-playwright/steps/ui/public.ts b/tests/e2e-playwright/steps/ui/public.ts new file mode 100644 index 00000000000..888a7952a55 --- /dev/null +++ b/tests/e2e-playwright/steps/ui/public.ts @@ -0,0 +1,23 @@ +import { objects } from '../../../e2e/support' +import { ActorsEnvironment, LinksEnvironment } from '../../../e2e/support/environment' + +export async function openPublicLink({ + actorsEnvironment, + linksEnvironment, + stepUser, + name +}: { + actorsEnvironment: ActorsEnvironment + linksEnvironment: LinksEnvironment + stepUser: string + name: string +}): Promise { + const { page } = await actorsEnvironment.createActor({ + key: stepUser, + namespace: actorsEnvironment.generateNamespace(stepUser, stepUser) + }) + + const { url } = linksEnvironment.getLink({ name }) + const pageObject = new objects.applicationFiles.page.Public({ page }) + await pageObject.open({ url }) +} diff --git a/tests/e2e-playwright/steps/ui/resources.ts b/tests/e2e-playwright/steps/ui/resources.ts new file mode 100644 index 00000000000..0d08404ca22 --- /dev/null +++ b/tests/e2e-playwright/steps/ui/resources.ts @@ -0,0 +1,44 @@ +import { objects } from '../../../e2e/support' +import { ActorsEnvironment, FilesEnvironment } from '../../../e2e/support/environment' + +export async function uploadResource({ + actorsEnvironment, + filesEnvironment, + stepUser, + resource, + to, + type, + option +}: { + actorsEnvironment: ActorsEnvironment + filesEnvironment: FilesEnvironment + stepUser: string + resource: string + to: string + type?: string + option?: string +}): Promise { + const { page } = actorsEnvironment.getActor({ key: stepUser }) + const resourceObject = new objects.applicationFiles.Resource({ page }) + await resourceObject.upload({ + to: to, + resources: [filesEnvironment.getFile({ name: resource })], + option: option, + type: type + }) +} + +export async function isAbleToEditFileOrFolder({ + actorsEnvironment, + stepUser, + resource +}: { + actorsEnvironment: ActorsEnvironment + stepUser: string + resource: string +}): Promise { + const { page } = actorsEnvironment.getActor({ key: stepUser }) + const resourceObject = new objects.applicationFiles.Resource({ page }) + const userCanEdit = await resourceObject.canManageResource({ resource }) + return userCanEdit +} diff --git a/tests/e2e-playwright/steps/ui/session.ts b/tests/e2e-playwright/steps/ui/session.ts new file mode 100644 index 00000000000..3d168f9507f --- /dev/null +++ b/tests/e2e-playwright/steps/ui/session.ts @@ -0,0 +1,73 @@ +import { config } from '../../../e2e/config.js' +import { api, objects } from '../../../e2e/support' +import { ActorsEnvironment, UsersEnvironment } from '../../../e2e/support/environment' +import { User } from '../../../e2e/support/types' + +async function createNewSession(actorsEnvironment: ActorsEnvironment, stepUser: string) { + const { page } = await actorsEnvironment.createActor({ + key: stepUser, + namespace: actorsEnvironment.generateNamespace(stepUser, stepUser) + }) + return new objects.runtime.Session({ page }) +} + +async function initUserStates(userKey: string, user: User, usersEnvironment: UsersEnvironment) { + const userInfo = await api.graph.getMeInfo(user) + usersEnvironment.storeCreatedUser(userKey, { + ...user, + uuid: userInfo.id, + email: userInfo.mail + }) + usersEnvironment.saveUserState(userKey, { + language: userInfo.preferredLanguage, + autoAcceptShare: await api.settings.getAutoAcceptSharesValue(user) + }) +} + +export async function logInUser({ + usersEnvironment, + actorsEnvironment, + stepUser +}: { + usersEnvironment: UsersEnvironment + actorsEnvironment: ActorsEnvironment + stepUser: string +}): Promise { + const sessionObject = await createNewSession(actorsEnvironment, stepUser) + const { page } = actorsEnvironment.getActor({ key: stepUser }) + + let user = null + if (stepUser === 'Admin' || config.predefinedUsers) { + user = usersEnvironment.getUser({ key: stepUser }) + } else { + user = usersEnvironment.getCreatedUser({ key: stepUser }) + } + + await page.goto(config.baseUrl) + await sessionObject.login(user) + + await page.locator('#web-content').waitFor() + + // initialize user states: uuid, language, auto-sync + if (config.predefinedUsers) { + await initUserStates(stepUser, user, usersEnvironment) + // test should run with English language + await api.settings.changeLanguage({ user, language: 'en' }) + await page.reload({ waitUntil: 'load' }) + } +} + +export async function logOutUser({ + actorsEnvironment, + stepUser +}: { + actorsEnvironment: ActorsEnvironment + stepUser: string +}): Promise { + const actor = actorsEnvironment.getActor({ key: stepUser }) + const canLogout = !!(await actor.page.locator('#_userMenuButton').count()) + + const sessionObject = new objects.runtime.Session({ page: actor.page }) + canLogout && (await sessionObject.logout()) + await actor.close() +} diff --git a/tests/e2e-playwright/steps/ui/shares.ts b/tests/e2e-playwright/steps/ui/shares.ts new file mode 100644 index 00000000000..6db20484bee --- /dev/null +++ b/tests/e2e-playwright/steps/ui/shares.ts @@ -0,0 +1,114 @@ +import { objects } from '../../../e2e/support' +import { ActorsEnvironment, UsersEnvironment } from '../../../e2e/support/environment' +import { getDynamicRoleIdByName, ResourceType } from '../../../e2e/support/api/share/share' +import { + CollaboratorType, + ICollaborator +} from '../../../e2e/support/objects/app-files/share/collaborator' + +const parseShareTable = function ( + usersEnvironment: UsersEnvironment, + resource: string, + recipient: string, + type: string, + role: string, + resourceType: string, + expirationDate?: string, + shareType?: string +) { + const stepTable = [ + { + resource, + recipient, + type, + role, + resourceType, + expirationDate, + shareType + } + ] + return stepTable.reduce>((acc, stepRow) => { + const { resource, recipient, type, role, resourceType, expirationDate, shareType } = stepRow + + if (!acc[resource]) { + acc[resource] = [] + } + + acc[resource].push({ + collaborator: + type === 'group' + ? usersEnvironment.getGroup({ key: recipient }) + : usersEnvironment.getUser({ key: recipient }), + role, + type: type as CollaboratorType, + resourceType, + expirationDate, + shareType + }) + + return acc + }, {}) +} + +export async function navigateToSharedWithMePage({ + actorsEnvironment, + stepUser +}: { + actorsEnvironment: ActorsEnvironment + stepUser: string +}): Promise { + const { page } = actorsEnvironment.getActor({ key: stepUser }) + const pageObject = new objects.applicationFiles.page.shares.WithMe({ page }) + await pageObject.navigate() +} + +export async function updateShareeRole({ + usersEnvironment, + actorsEnvironment, + stepUser, + resource, + recipient, + type, + role, + resourceType, + expirationDate, + shareType +}: { + usersEnvironment: UsersEnvironment + actorsEnvironment: ActorsEnvironment + stepUser: string + resource: string + recipient: string + type: string + role: string + resourceType: string + expirationDate?: string + shareType?: string +}) { + const { page } = actorsEnvironment.getActor({ key: stepUser }) + const shareObject = new objects.applicationFiles.Share({ page }) + const shareInfo = parseShareTable( + usersEnvironment, + resource, + recipient, + type, + role, + resourceType, + expirationDate, + shareType + ) + const sharer = usersEnvironment.getUser({ key: stepUser }) + + for (const [resource, shareObj] of Object.entries(shareInfo)) { + const roleId = await getDynamicRoleIdByName( + sharer, + shareObj[0].role, + shareObj[0].resourceType as ResourceType + ) + shareObj.forEach((item) => (item.role = roleId)) + await shareObject.changeShareeRole({ + resource, + recipients: shareObj + }) + } +} diff --git a/tests/e2e/support/api/davSpaces/spaces.ts b/tests/e2e/support/api/davSpaces/spaces.ts index fb4552fa2f8..c5e2a82751b 100644 --- a/tests/e2e/support/api/davSpaces/spaces.ts +++ b/tests/e2e/support/api/davSpaces/spaces.ts @@ -3,7 +3,7 @@ import { User } from '../../types' import join from 'join-path' import { getSpaceIdBySpaceName } from '../graph' import convert from 'xml-js' -import _ from 'lodash-es/object' +import _ from 'lodash-es/object.js' import { createTagsForResource } from '../graph/utils' export const folderExists = async ({