diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml new file mode 100644 index 000000000..6f70f9fae --- /dev/null +++ b/.github/workflows/playwright-tests.yml @@ -0,0 +1,40 @@ +name: Playwright tests + +on: + push: + branches: + - '[0-9]+.[0-9]+' + pull_request: ~ + +jobs: + playwright-oss: + name: "Playwright/OSS" + uses: ibexa/gh-workflows/.github/workflows/playwright-browser-tests.yml@ibx-11740-playwright + with: + project-edition: 'oss' + test-package: 'admin-ui' + secrets: inherit + + playwright-headless: + name: "Playwright/Headless" + uses: ibexa/gh-workflows/.github/workflows/playwright-browser-tests.yml@ibx-11740-playwright + with: + project-edition: 'headless' + test-package: 'admin-ui' + secrets: inherit + + playwright-experience: + name: "Playwright/Experience" + uses: ibexa/gh-workflows/.github/workflows/playwright-browser-tests.yml@ibx-11740-playwright + with: + project-edition: 'experience' + test-package: 'admin-ui' + secrets: inherit + + playwright-commerce: + name: "Playwright/Commerce" + uses: ibexa/gh-workflows/.github/workflows/playwright-browser-tests.yml@ibx-11740-playwright + with: + project-edition: 'commerce' + test-package: 'admin-ui' + secrets: inherit diff --git a/package.json b/package.json index bd4555285..7161accf3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,11 @@ "ts-test": "tsc --noEmit", "eslint-test": "eslint \"./src/bundle/Resources/**/*.{js,ts}\" \"./src/bundle/ui-dev/**/*.{js,tsx}\"", "prettier-test": "yarn prettier \"./src/bundle/Resources/**/*.{js,ts,scss}\" \"./src/bundle/ui-dev/**/*.{js,tsx}\" --check", - "postinstall": "yarn ibexa-generate-tsconfig --use-root-project-tsconfig" + "postinstall": "yarn ibexa-generate-tsconfig --use-root-project-tsconfig", + "pw:install": "npm --prefix tests/playwright-tests install", + "pw:test": "npm --prefix tests/playwright-tests test --", + "pw:test:headless": "npm --prefix tests/playwright-tests run test:headless --", + "pw:test:experience": "npm --prefix tests/playwright-tests run test:experience --", + "pw:test:commerce": "npm --prefix tests/playwright-tests run test:commerce --" } } diff --git a/tests/playwright-tests/.gitignore b/tests/playwright-tests/.gitignore new file mode 100644 index 000000000..948792760 --- /dev/null +++ b/tests/playwright-tests/.gitignore @@ -0,0 +1,9 @@ +.env +.auth/ +playwright-report/ +test-results/ +node_modules/ +dist/ +# un-ignore files excluded by root .gitignore +!tsconfig.json +!package-lock.json diff --git a/tests/playwright-tests/Tests/Trash.spec.ts b/tests/playwright-tests/Tests/Trash.spec.ts new file mode 100644 index 000000000..9f50b028d --- /dev/null +++ b/tests/playwright-tests/Tests/Trash.spec.ts @@ -0,0 +1,135 @@ +import { test, expect } from '@playwright/test'; +import { TrashPage, ContentManagementPage, IbexaApiClient } from '@ibexa/cohesivo-playwright'; + + +test.describe('Trash management', { tag: ['@IbexaOSS', '@IbexaHeadless', '@IbexaExperience', '@IbexaCommerce'] }, () => { + let api: IbexaApiClient; + let trashTestLocationId: number; + let trashTestContentId: number; + let runId: string; + + test.beforeAll(async () => { + api = new IbexaApiClient(); + await api.init(); + runId = Date.now().toString().slice(-6); + + trashTestContentId = await api.createFolder(`TrashTest${runId}`, 2); + trashTestLocationId = await api.getMainLocationId(trashTestContentId); + }); + + test.afterAll(async () => { + if (api && trashTestContentId) { + await api.deleteContent(trashTestContentId); + } + }); + + test('Trash can be emptied', async ({ page }) => { + const childId = await api.createFolder(`FolderToTrash${runId}`, trashTestLocationId); + const childLocId = await api.getMainLocationId(childId); + + const contentPage = new ContentManagementPage(page); + await contentPage.open(childId, childLocId); + await contentPage.sendToTrash(); + + const trash = new TrashPage(page); + await trash.open(); + await trash.assertNotEmpty(); + await trash.emptyTrash(); + await trash.assertEmpty(); + }); + + test('Content can be moved to trash', async ({ page }) => { + const name = `FolderToTrashManually${runId}`; + const childId = await api.createFolder(name, trashTestLocationId); + const childLocId = await api.getMainLocationId(childId); + + const contentPage = new ContentManagementPage(page); + await contentPage.open(childId, childLocId); + await contentPage.sendToTrash(); + + await contentPage.notifications.assertSuccess(`Location '${name}' moved to Trash`); + + const trash = new TrashPage(page); + await trash.open(); + await trash.assertItemInTrash(name); + }); + + test('Element in trash can be deleted', async ({ page }) => { + const name = `DeleteFromTrash${runId}`; + const childId = await api.createFolder(name, trashTestLocationId); + const childLocId = await api.getMainLocationId(childId); + + const contentPage = new ContentManagementPage(page); + await contentPage.open(childId, childLocId); + await contentPage.sendToTrash(); + + const trash = new TrashPage(page); + await trash.open(); + await trash.assertItemInTrash(name); + await trash.deleteFromTrash([name]); + await trash.notifications.assertSuccess('Deleted selected item(s) from Trash'); + await trash.assertItemNotInTrash(name); + }); + + test('Element in trash can be restored', async ({ page }) => { + const name = `RestoreFromTrash${runId}`; + const childId = await api.createFolder(name, trashTestLocationId); + const childLocId = await api.getMainLocationId(childId); + + const contentPage = new ContentManagementPage(page); + await contentPage.open(childId, childLocId); + await contentPage.sendToTrash(); + + const trash = new TrashPage(page); + await trash.open(); + await trash.assertItemInTrash(name); + await trash.restoreFromTrash([name]); + await trash.notifications.assertSuccess('Restored content to its original Location'); + await trash.assertItemNotInTrash(name); + }); + + test('Element in trash can be restored under new location', async ({ page }) => { + const name = `RestoreFromTrashNewLocation${runId}`; + const childId = await api.createFolder(name, trashTestLocationId); + const childLocId = await api.getMainLocationId(childId); + + const contentPage = new ContentManagementPage(page); + await contentPage.open(childId, childLocId); + await contentPage.sendToTrash(); + + const trash = new TrashPage(page); + await trash.open(); + await trash.assertItemInTrash(name); + await trash.restoreUnderNewLocation([name], 'Media/Files'); + + await trash.notifications.assertSuccess("Restored content under Location 'Files'"); + await trash.assertItemNotInTrash(name); + + // Verify the content actually landed under Media/Files (path resolution throws if absent) + const restoredContentId = await api.getContentIdByPath(`Media/Files/${name}`); + expect(restoredContentId).toBe(childId); + + await api.deleteContent(childId); + }); + + test('Element in trash can be found by search', async ({ page }) => { + const name1 = `TrashSearch1${runId}`; + const name2 = `TrashSearch2${runId}`; + const childId1 = await api.createFolder(name1, trashTestLocationId); + const childLocId1 = await api.getMainLocationId(childId1); + const childId2 = await api.createFolder(name2, trashTestLocationId); + const childLocId2 = await api.getMainLocationId(childId2); + + const contentPage = new ContentManagementPage(page); + await contentPage.open(childId1, childLocId1); + await contentPage.sendToTrash(); + await contentPage.open(childId2, childLocId2); + await contentPage.sendToTrash(); + + const trash = new TrashPage(page); + await trash.open(); + await trash.searchInTrash(name1); + await trash.assertItemInTrash(name1); + await trash.assertItemNotInTrash(name2); + }); +}); diff --git a/tests/playwright-tests/package-lock.json b/tests/playwright-tests/package-lock.json new file mode 100644 index 000000000..4d9b7f192 --- /dev/null +++ b/tests/playwright-tests/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "@ibexa/admin-ui-playwright-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ibexa/admin-ui-playwright-tests", + "version": "1.0.0", + "dependencies": { + "@ibexa/cohesivo-playwright": "file:../../../cohesivo-playwright" + } + }, + "../../../cohesivo-playwright": { + "name": "@ibexa/cohesivo-playwright", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.62.1", + "@types/node": "^20.0.0", + "dotenv": "^17.4.2", + "typescript": "^5.5.0" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ibexa/cohesivo-playwright": { + "resolved": "../../../cohesivo-playwright", + "link": true + } + } +} diff --git a/tests/playwright-tests/package.json b/tests/playwright-tests/package.json new file mode 100644 index 000000000..d9cfbd8c3 --- /dev/null +++ b/tests/playwright-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "@ibexa/admin-ui-playwright-tests", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "NODE_PATH=./node_modules/@ibexa/cohesivo-playwright/node_modules ./node_modules/@ibexa/cohesivo-playwright/node_modules/.bin/playwright test", + "test:headless": "NODE_PATH=./node_modules/@ibexa/cohesivo-playwright/node_modules ./node_modules/@ibexa/cohesivo-playwright/node_modules/.bin/playwright test --project=headless", + "test:experience": "NODE_PATH=./node_modules/@ibexa/cohesivo-playwright/node_modules ./node_modules/@ibexa/cohesivo-playwright/node_modules/.bin/playwright test --project=experience", + "test:commerce": "NODE_PATH=./node_modules/@ibexa/cohesivo-playwright/node_modules ./node_modules/@ibexa/cohesivo-playwright/node_modules/.bin/playwright test --project=commerce", + "typecheck": "./node_modules/@ibexa/cohesivo-playwright/node_modules/.bin/tsc --noEmit" + }, + "dependencies": { + "@ibexa/cohesivo-playwright": "file:../../../cohesivo-playwright" + } +} diff --git a/tests/playwright-tests/playwright.config.ts b/tests/playwright-tests/playwright.config.ts new file mode 100644 index 000000000..b5d98db73 --- /dev/null +++ b/tests/playwright-tests/playwright.config.ts @@ -0,0 +1,3 @@ +import { defineIbexaConfig } from '@ibexa/cohesivo-playwright'; + +export default defineIbexaConfig({ testDir: './Tests' }); diff --git a/tests/playwright-tests/tsconfig.json b/tests/playwright-tests/tsconfig.json new file mode 100644 index 000000000..6406e6218 --- /dev/null +++ b/tests/playwright-tests/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./node_modules/@ibexa/cohesivo-playwright/tsconfig.playwright.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "Tests/**/*.ts", + "Utils/**/*.ts", + "playwright.config.ts" + ], + "exclude": ["node_modules", "dist"] +}