diff --git a/backend/Makefile b/backend/Makefile index 12af7a38..c06507eb 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -170,6 +170,10 @@ solr-activate-and-reindex: $(VENV_FOLDER) instance/etc/zope.conf ## Activate sol acceptance-backend-start: ## Start backend acceptance server ZSERVER_HOST=0.0.0.0 ZSERVER_PORT=55001 LISTEN_PORT=55001 $(BIN_FOLDER)/robot-server kitconcept.intranet.testing.ROBOT_TESTING +.PHONY: acceptance-deepl-backend-start +acceptance-deepl-backend-start: ## Start backend acceptance server for the DeepL tests (multilingual) + ZSERVER_HOST=0.0.0.0 ZSERVER_PORT=55001 LISTEN_PORT=55001 $(BIN_FOLDER)/robot-server kitconcept.intranet.testing.DEEPL_ROBOT_TESTING + .PHONY: acceptance-backend-start acceptance-a11y-backend-start: ## Start backend a11y acceptance server ZSERVER_HOST=0.0.0.0 ZSERVER_PORT=55001 LISTEN_PORT=55001 $(BIN_FOLDER)/robot-server kitconcept.intranet.testing.A11Y_TESTING diff --git a/backend/news/+addSupportForDeepLBlocksTesting.internal b/backend/news/+addSupportForDeepLBlocksTesting.internal new file mode 100644 index 00000000..23e093d1 --- /dev/null +++ b/backend/news/+addSupportForDeepLBlocksTesting.internal @@ -0,0 +1 @@ +Add support for acceptance tests for blocks translation using DeepL @Tishasoumya-02 \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 27a4518b..cad8f4d6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "collective-solr==10.1.1", "kitconcept.solr==2.0.0a14", "kitconcept.contactblock==1.0.0a6", + "kitconcept.deepl", ] [dependency-groups] @@ -647,3 +648,6 @@ constraint-dependencies = [ "zope.traversing==5.0", "zope.viewlet==5.0", ] + +[tool.uv.sources] +kitconcept-deepl = { path = "../frontend/packages/volto-kitconcept-deepl/backend" } \ No newline at end of file diff --git a/backend/src/kitconcept/intranet/testing/__init__.py b/backend/src/kitconcept/intranet/testing/__init__.py index d2aaf527..1be1aaff 100644 --- a/backend/src/kitconcept/intranet/testing/__init__.py +++ b/backend/src/kitconcept/intranet/testing/__init__.py @@ -1,11 +1,13 @@ from kitconcept.intranet.testing.a11y import A11Y_TESTING from kitconcept.intranet.testing.base import FUNCTIONAL_TESTING from kitconcept.intranet.testing.base import INTEGRATION_TESTING +from kitconcept.intranet.testing.deepl import DEEPL_ROBOT_TESTING from kitconcept.intranet.testing.robot import ROBOT_TESTING __all__ = [ "A11Y_TESTING", + "DEEPL_ROBOT_TESTING", "FUNCTIONAL_TESTING", "INTEGRATION_TESTING", "ROBOT_TESTING", diff --git a/backend/src/kitconcept/intranet/testing/deepl.py b/backend/src/kitconcept/intranet/testing/deepl.py new file mode 100644 index 00000000..a7e917c7 --- /dev/null +++ b/backend/src/kitconcept/intranet/testing/deepl.py @@ -0,0 +1,104 @@ +"""Acceptance testing layer for the DeepL translation tests. + +The intranet itself is monolingual, so the regular ``ROBOT_TESTING`` layer +stays English-only. The DeepL acceptance tests need a multilingual (en/de) +site with the DeepLSettings control panel, so they get their own layer that +installs the ``kitconcept.deepl`` profiles on top of the base fixture. +""" + +from kitconcept.intranet.testing.base import BaseFixture +from plone.app.robotframework.autologin import AutoLogin +from plone.app.robotframework.content import Content +from plone.app.robotframework.genericsetup import GenericSetup +from plone.app.robotframework.i18n import I18N +from plone.app.robotframework.mailhost import MockMailHost +from plone.app.robotframework.quickinstaller import QuickInstaller +from plone.app.robotframework.remote import RemoteLibraryLayer +from plone.app.robotframework.server import Zope2ServerRemote +from plone.app.robotframework.testing import WSGI_SERVER_SINGLE_THREADED_FIXTURE +from plone.app.robotframework.testing import MockMailHostLayer as BaseMailLayer +from plone.app.robotframework.testing import PloneRobotFixture +from plone.app.robotframework.users import Users +from plone.app.testing import applyProfile +from plone.app.testing import FunctionalTesting +from plone.app.testing.interfaces import SITE_OWNER_NAME +from plone.testing import zope +from zope.globalrequest import setRequest + + +ANSWERS_CONTENT = { + "site_id": "plone", + "title": "Intranet", + "description": "Site created with A Plone distribution for Intranets with Plone. Created by kitconcept.", # noQA: E501 + "available_languages": ["en", "de"], + "portal_timezone": "Europe/Berlin", + "workflow": "public", + "setup_content": False, + "authentication": {"provider": "internal"}, +} + + +class DeepLContentFixture(BaseFixture): + sites = (("kitconcept-intranet", ANSWERS_CONTENT),) + # Load kitconcept.deepl ZCML so its control panel / services are available. + internal_packages = BaseFixture.internal_packages + ("kitconcept.deepl",) + + def setUpDefaultContent(self, app): + super().setUpDefaultContent(app) + # Install the DeepL profiles on each created site so that the + # multilingual (en/de) language roots and the DeepLSettings control + # panel exist for the DeepL acceptance tests. + setRequest(app.REQUEST) + zope.login(app["acl_users"], SITE_OWNER_NAME) + for _distribution_name, answers in self.sites: + portal = app[answers["site_id"]] + applyProfile(portal, "kitconcept.deepl:initial") + applyProfile(portal, "kitconcept.deepl:default") + zope.logout() + setRequest(None) + + +DEEPL_CONTENT_FIXTURE = DeepLContentFixture() + + +class DeepLMockMailHostLayer(BaseMailLayer): + defaultBases = (DEEPL_CONTENT_FIXTURE,) + + +DEEPL_MOCK_MAILHOST_FIXTURE = DeepLMockMailHostLayer() + + +class DeepLRobotFixture(PloneRobotFixture): + """DeepL acceptance fixture, using robot framework, for kitconcept.intranet.""" + + defaultBases = (DEEPL_MOCK_MAILHOST_FIXTURE,) + + def setUpPloneSite(self, portal): + super().setUpPloneSite(portal) + + +DEEPL_ROBOT_FIXTURE = DeepLRobotFixture() + +DEEPL_REMOTE_LIBRARY_BUNDLE_FIXTURE = RemoteLibraryLayer( + bases=(DEEPL_CONTENT_FIXTURE,), + libraries=( + AutoLogin, + QuickInstaller, + GenericSetup, + Content, + Users, + I18N, + MockMailHost, + Zope2ServerRemote, + ), + name="DeepLRemoteLibraryBundle:RobotRemote", +) + +DEEPL_ROBOT_TESTING = FunctionalTesting( + bases=( + DEEPL_ROBOT_FIXTURE, + DEEPL_REMOTE_LIBRARY_BUNDLE_FIXTURE, + WSGI_SERVER_SINGLE_THREADED_FIXTURE, + ), + name="Intranet:DeepLRobot", +) diff --git a/backend/uv.lock b/backend/uv.lock index 905c2e07..b16dcd9b 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1338,6 +1338,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/ad/dc41564c61260f154108c3b0c8903076d92190982c3e8db52459307bdda5/kitconcept_core-2.0.0a20-py3-none-any.whl", hash = "sha256:cab7064ddb3e5082ee28ff61cbfda76cb3564d1409131d9b1ef73edd7f387648", size = 248415, upload-time = "2026-06-15T10:18:24.126Z" }, ] +[[package]] +name = "kitconcept-deepl" +source = { directory = "../frontend/packages/volto-kitconcept-deepl/backend" } +dependencies = [ + { name = "plone-api" }, + { name = "plone-restapi" }, + { name = "plone-volto" }, + { name = "products-cmfplone" }, +] + +[package.metadata] +requires-dist = [ + { name = "horse-with-no-namespace", marker = "extra == 'test'" }, + { name = "plone-api" }, + { name = "plone-app-testing", marker = "extra == 'test'" }, + { name = "plone-restapi" }, + { name = "plone-restapi", extras = ["test"], marker = "extra == 'test'" }, + { name = "plone-volto" }, + { name = "products-cmfplone" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "pytest-plone", marker = "extra == 'test'", specifier = ">=1.0.0a2" }, +] +provides-extras = ["test"] + [[package]] name = "kitconcept-intranet" source = { editable = "." } @@ -1345,6 +1370,7 @@ dependencies = [ { name = "collective-solr" }, { name = "kitconcept-contactblock" }, { name = "kitconcept-core" }, + { name = "kitconcept-deepl" }, { name = "kitconcept-solr" }, { name = "pas-plugins-authomatic" }, { name = "pas-plugins-keycloakgroups" }, @@ -1387,6 +1413,7 @@ requires-dist = [ { name = "collective-solr", specifier = "==10.1.1" }, { name = "kitconcept-contactblock", specifier = "==1.0.0a6" }, { name = "kitconcept-core", specifier = "==2.0.0a20" }, + { name = "kitconcept-deepl", directory = "../frontend/packages/volto-kitconcept-deepl/backend" }, { name = "kitconcept-solr", specifier = "==2.0.0a14" }, { name = "pas-plugins-authomatic", specifier = "==2.0.0" }, { name = "pas-plugins-keycloakgroups", specifier = "==1.0.0b1" }, diff --git a/frontend/Makefile b/frontend/Makefile index 49ea06ae..8108ecf9 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -24,6 +24,9 @@ IMAGE_NAME=ghcr.io/kitconcept/kitconcept-intranet-frontend IMAGE_TAG=latest VOLTO_VERSION = $(shell cat ./mrs.developer.json | python -c "import sys, json; print(json.load(sys.stdin)['core']['tag'])") +# DeepL API key for the DeepL acceptance tests; override via environment. +DEEPL_API_KEY ?= + .PHONY: help help: ## Show this help @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" @@ -131,11 +134,16 @@ acceptance-frontend-prod-start: ## Start acceptance frontend in production mode .PHONY: acceptance-test acceptance-test: ## Start Cypress in interactive mode - pnpm --filter @plone/volto exec cypress open --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx},excludeSpecPattern='$(CURRENT_DIR)'/cypress/tests/{a11y,visual}*/**/*.{js,jsx,ts,tsx}' + pnpm --filter @plone/volto exec cypress open --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx},excludeSpecPattern='$(CURRENT_DIR)'/cypress/tests/{a11y,visual,deepl}*/**/*.{js,jsx,ts,tsx}' .PHONY: ci-acceptance-test ci-acceptance-test: ## Run cypress tests in headless mode for CI - pnpm --filter @plone/volto exec cypress run --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx},excludeSpecPattern='$(CURRENT_DIR)'/cypress/tests/{a11y,visual}*/**/*.{js,jsx,ts,tsx}' + pnpm --filter @plone/volto exec cypress run --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx},excludeSpecPattern='$(CURRENT_DIR)'/cypress/tests/{a11y,visual,deepl}*/**/*.{js,jsx,ts,tsx}' + +## DeepL acceptance tests (require DEEPL_API_KEY) +.PHONY: acceptance-deepl-test +acceptance-deepl-test: ## Start DeepL Cypress in interactive mode + pnpm --filter @plone/volto exec cypress open --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/deepl/**/*.{js,jsx,ts,tsx}' --env DEEPL_API_KEY=$(DEEPL_API_KEY) ## A11y tests .PHONY: acceptance-a11y-frontend-dev-start diff --git a/frontend/cypress/fixtures/halfdome2022.jpg b/frontend/cypress/fixtures/halfdome2022.jpg new file mode 100644 index 00000000..58c9411c Binary files /dev/null and b/frontend/cypress/fixtures/halfdome2022.jpg differ diff --git a/frontend/cypress/support/commands.js b/frontend/cypress/support/commands.js index a43c1471..cb51971a 100644 --- a/frontend/cypress/support/commands.js +++ b/frontend/cypress/support/commands.js @@ -1,5 +1,41 @@ import '@plone/volto/cypress/add-commands'; +// --- DEEPL ------------------------------------------------------------------- +// Configure the DeepL control panel with the API key and log in. +// Common setup for all block translation acceptance tests. +Cypress.Commands.add('setupDeepL', () => { + const apiKey = Cypress.env('DEEPL_API_KEY'); + if (!apiKey) { + throw new Error('DEEPL_API_KEY environment variable is not set'); + } + + cy.request({ + url: '/++api++/@controlpanels/DeepLSettings', + method: 'PATCH', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: 'Basic YWRtaW46c2VjcmV0', + }, + body: { + deepl_api_auth_token: apiKey, + deepl_api_timeout: 10, + }, + }).then((response) => { + expect(response.status).to.eq(204); + }); + + cy.autologin(); +}); + +// Open "Manage Translations" and trigger the automatic translation, +// landing on the German (/de) add form. Common to all block tests. +Cypress.Commands.add('createTranslation', () => { + cy.get('#toolbar-more').click(); + cy.findByText('Manage Translations').should('be.visible').click(); + cy.get('.manage-multilingual-tools .buttons a').eq(1).click(); +}); + // --- SOLR -------------------------------------------------------------------- Cypress.Commands.add('reindexSolr', () => { const log = Cypress.log({ diff --git a/frontend/cypress/tests/deepl/accordionBlock.cy.js b/frontend/cypress/tests/deepl/accordionBlock.cy.js new file mode 100644 index 00000000..356541a9 --- /dev/null +++ b/frontend/cypress/tests/deepl/accordionBlock.cy.js @@ -0,0 +1,88 @@ +context('Accordion Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-link-target', + contentTitle: 'Link Target', + path: '/en', + }); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create an accordion block (headline, panel title, nested text) and translate it', () => { + cy.visit('/en/my-link-target'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('accordion'); + + cy.get('.accordion:nth-child(2) > .title input').type('Same Title'); + cy.get('.accordion:nth-child(3) > .title input').type('Same Title'); + cy.get('.accordion:nth-child(4) > .title input').type('Different Title'); + cy.get('.text-slate-editor-inner') + .eq(0) + .click() + .type('Hello world{enter}', { + delay: 50, + }); + + cy.focused().type('/image{enter}'); + cy.findAllByLabelText('Enter a URL to an image').filter(':visible').click(); + cy.get('.ui.input.editor-link.input-anchorlink-theme input').type( + 'https://github.com/plone/volto/raw/main/logos/volto-colorful.png{enter}', + ); + cy.get('.accordion:nth-child(2) .content .block.image figure img').should( + 'exist', + ); + + cy.get( + '#blockform-fieldset-default .field-wrapper-title #field-title', + ).type('Hello image title'); + cy.get( + '#blockform-fieldset-default .field-wrapper-description #field-description', + ).type('Hello image description'); + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Link Target'); + cy.wait(500); + cy.get('[aria-label="Select Link Target"]').dblclick(); + + cy.get('#toolbar-save').click(); + cy.createTranslation(); + cy.wait(2000); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.accordion-block .block.image figcaption .title').should( + 'contain', + 'Hallo', + ); + cy.get('.accordion-block .block.image figcaption .description').should( + 'contain', + 'Hallo', + ); + cy.get('.accordion-block .block.image a') + .should('have.attr', 'href') + .and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/bannerBlock.cy.js b/frontend/cypress/tests/deepl/bannerBlock.cy.js new file mode 100644 index 00000000..79bed7b8 --- /dev/null +++ b/frontend/cypress/tests/deepl/bannerBlock.cy.js @@ -0,0 +1,56 @@ +context('Banner Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a banner block (text + additionalText) and translate it', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('Banner'); + + cy.get('input[type="file"]').attachFile('halfdome2022.jpg', { + subjectType: 'input', + encoding: 'utf8', + }); + cy.wait(500); + + cy.get('.field-wrapper-text #field-text').type('Hello'); + cy.get('.field-wrapper-additionalText #field-additionalText').type( + 'Hello world', + ); + + cy.get('#toolbar-save').click(); + + // translate the page to German + cy.createTranslation(); + + cy.get('#page-add .block.banner .banner-inner-container .text p', { + timeout: 10000, + }) + .eq(0) + .should('contain', 'Hallo'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.banner .banner-inner-container .text p') + .eq(0) + .should('contain', 'Hallo'); + cy.get('.block.banner .banner-inner-container .text p') + .eq(1) + .should('contain', 'Hallo'); + }); +}); diff --git a/frontend/cypress/tests/deepl/buttonBlock.cy.js b/frontend/cypress/tests/deepl/buttonBlock.cy.js new file mode 100644 index 00000000..b9da8ca9 --- /dev/null +++ b/frontend/cypress/tests/deepl/buttonBlock.cy.js @@ -0,0 +1,61 @@ +context('Button Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-link-target', + contentTitle: 'Link Target', + path: '/en', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a button block (title + href) and translate it', () => { + cy.visit('/en/my-link-target'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('Button'); + + cy.get( + '#blockform-fieldset-default .field-wrapper-title #field-title', + ).type('Read more'); + + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Link Target'); + cy.wait(500); + cy.get('[aria-label="Select Link Target"]').dblclick(); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.__button a') + .should('have.attr', 'href') + .and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/carouselBlock.cy.js b/frontend/cypress/tests/deepl/carouselBlock.cy.js new file mode 100644 index 00000000..274bc8d2 --- /dev/null +++ b/frontend/cypress/tests/deepl/carouselBlock.cy.js @@ -0,0 +1,187 @@ +context('Carousel Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + cy.viewport('macbook-16'); + + cy.setupDeepL(); + + cy.fixture('halfdome2022.jpg', 'base64').then((fileContent) => { + cy.createContent({ + contentType: 'Image', + contentId: 'my-image', + contentTitle: 'My Image', + contentDescription: 'A picture of Half Dome', + path: '/en', + bodyModifier(body) { + body.image = { + data: fileContent, + encoding: 'base64', + filename: 'image.png', + 'content-type': 'image/png', + }; + return body; + }, + }); + }); + + cy.createContent({ + contentType: 'Document', + contentId: 'blue-orchids', + contentTitle: 'Blue Orchids', + contentDescription: 'are growing on the mountain tops', + preview_image_link: { + '@id': '/en/my-image', + }, + path: '/en', + }); + + // The Document that hosts the carousel block. + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Document', + path: '/en', + }); + }); + + it('As editor I can add a carousel block and translate it', () => { + // Translate the image so its own title/description exist in German. + cy.visit('/en/my-image'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/blue-orchids'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.get('.button .block-add-button').click({ force: true }); + cy.get('.blocks-chooser .mostUsed .button.carousel') + .contains('Carousel') + .click({ force: true }); + cy.get( + '.field-wrapper-columns .add-item-button-wrapper button[aria-label="Add item"]', + ) + .should('be.visible') + .click(); + + cy.get( + '.olw-item-wrapper .olw-item-title-bar[aria-label="Show item #"]', + ).click(); + cy.get( + '.olw-item-wrapper.active .objectbrowser-field[aria-labelledby^="fieldset-default-field-label-href-0-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Blue Orchids'); + cy.wait(500); + cy.get('[aria-label="Select Blue Orchids"]').dblclick(); + cy.wait(500); + cy.get( + '.olw-item-wrapper.active [class*="field-wrapper-overwrite"] input', + ).check({ force: true }); + cy.get( + '.olw-item-wrapper.active [class*="field-wrapper-title-"] input', + ).type(' Hello! Custom Teaser'); + cy.get( + '.olw-item-wrapper.active [class*="field-wrapper-description"] textarea', + ).type(' Hello world custom description'); + + cy.get( + '.olw-item-wrapper.active .objectbrowser-field[aria-labelledby^="fieldset-default-field-label-preview_image-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('My Image'); + cy.wait(500); + cy.get('[aria-label="Select My Image"]').dblclick(); + + cy.get( + '.olw-item-wrapper .olw-item-title-bar[aria-label="Show item #2"]', + ).click(); + cy.get( + '.olw-item-wrapper.active .objectbrowser-field[aria-labelledby^="fieldset-default-field-label-href-0-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Blue Orchids'); + cy.wait(500); + cy.get('[aria-label="Select Blue Orchids"]').dblclick(); + cy.wait(500); + cy.get( + '.olw-item-wrapper .olw-item-title-bar[aria-label="Show item #3"]', + ).click(); + cy.get( + '.olw-item-wrapper.active .objectbrowser-field[aria-labelledby^="fieldset-default-field-label-href-0-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Blue Orchids'); + cy.wait(500); + cy.get('[aria-label="Select Blue Orchids"]').dblclick(); + cy.wait(500); + cy.get( + '.olw-item-wrapper .olw-item-title-bar[aria-label="Show item #4"]', + ).click(); + cy.get( + '.olw-item-wrapper.active .objectbrowser-field[aria-labelledby^="fieldset-default-field-label-href-0-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Blue Orchids'); + cy.wait(500); + cy.get('[aria-label="Select Blue Orchids"]').dblclick(); + cy.wait(500); + cy.get( + '.olw-item-wrapper .olw-item-title-bar[aria-label="Show item #5"]', + ).click(); + cy.get( + '.olw-item-wrapper.active .objectbrowser-field[aria-labelledby^="fieldset-default-field-label-href-0-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Blue Orchids'); + cy.wait(500); + cy.get('[aria-label="Select Blue Orchids"]').dblclick(); + cy.get('#field-headline').click().clear().type('Hello! Carousel Block'); + cy.get('#field-items_to_show').click(); + cy.findByText('2').click(); + + cy.get('#field-hide_description') + .check({ force: true }) + .should('be.checked'); + + cy.get('#field-hide_description') + .uncheck({ force: true }) + .should('not.be.checked'); + cy.get('#toolbar-save').click(); + + cy.createTranslation(); + + cy.get('#page-add h2.headline', { timeout: 10000 }).should( + 'contain', + 'Hallo', + ); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('h2.headline').should('contain', 'Hallo'); + cy.get('.block.teaser:first .card-summary .title') + .should('be.visible') + .and('contain', 'Hallo') + .and('not.contain', 'Hello! Custom Teaser'); + cy.get('.block.teaser:first .card-summary p') + .should('be.visible') + .and('not.contain', 'Hello world custom description'); + cy.contains('.block.teaser .card-summary .title', 'Blaue Orchideen').should( + 'be.visible', + ); + cy.get('.block.teaser .image-wrapper img').first().should('be.visible'); + cy.get('.block.teaser a') + .first() + .should('have.attr', 'href') + .and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/eventCalendarBlock.cy.js b/frontend/cypress/tests/deepl/eventCalendarBlock.cy.js new file mode 100644 index 00000000..8cce94b9 --- /dev/null +++ b/frontend/cypress/tests/deepl/eventCalendarBlock.cy.js @@ -0,0 +1,71 @@ +context('Event Calendar Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + cy.intercept('GET', '**/@querystring-search**').as('querySearch'); + + const now = new Date(); + const formatDate = (date) => date.toISOString().replace('.000Z', '+00:00'); + const eventStart = new Date(now.getFullYear(), now.getMonth(), 11); + const eventEnd = new Date(now.getFullYear(), now.getMonth(), 12); + + cy.setupDeepL(); + + cy.createContent({ + contentType: 'Event', + contentId: 'my-first-event', + contentTitle: 'My First Event', + path: '/en', + bodyModifier(body) { + body.start = formatDate(eventStart); + body.end = formatDate(eventEnd); + return body; + }, + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create an event calendar block (headline + facets title) and translate it', () => { + cy.visit('/en/my-first-event'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@content'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('event'); + + cy.get('#sidebar-properties #field-headline').type('Hello headline'); + + cy.get('#sidebar-properties .field-wrapper-facets button') + .findByText('Add Facet') + .click({ force: true }); + cy.get('[id^="field-field-1-"] .react-select__value-container').click(); + cy.get('.react-select__option').contains('Review state').click(); + + cy.get('#toolbar-save').click(); + cy.wait('@content'); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.search-block-event .headline').should('contain', 'Hallo'); + cy.get('.card-primary-link') + .should('have.attr', 'href') + .and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/gridBlock.cy.js b/frontend/cypress/tests/deepl/gridBlock.cy.js new file mode 100644 index 00000000..ace31033 --- /dev/null +++ b/frontend/cypress/tests/deepl/gridBlock.cy.js @@ -0,0 +1,106 @@ +context('Grid Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-link-target', + contentTitle: 'Link Target', + path: '/en', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a grid block (image, teaser, text + headline) and translate it', () => { + cy.visit('/en/my-link-target'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('grid'); + cy.findByText('3 columns').click(); + + cy.get('#field-headline').type('Hello'); + + cy.get('button[aria-label="Add block in position 0"]').click(); + cy.get('.blocks-chooser .mostUsed .button.image').click(); + cy.findAllByLabelText('Enter a URL to an image').filter(':visible').click(); + cy.get('.ui.input.editor-link.input-anchorlink-theme input').type( + `https://github.com/plone/volto/raw/main/logos/volto-colorful.png{enter}`, + ); + cy.get('.block.image figure img').should('exist'); + cy.get('.field-wrapper-alt #field-alt').type('Volto Logo'); + cy.get( + '#blockform-fieldset-default .field-wrapper-title #field-title', + ).type('Hello'); + cy.get( + '#blockform-fieldset-default .field-wrapper-description #field-description', + ).type('Hello world'); + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Link Target'); + cy.wait(500); + cy.get('[aria-label="Select Link Target"]').dblclick(); + + cy.get('button[aria-label="Add block in position 1"]').click(); + cy.get('.blocks-chooser .mostUsed .button.teaser').click(); + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Link Target'); + cy.wait(500); + cy.get('[aria-label="Select Link Target"]').dblclick(); + + // column 2: text (slate) block + cy.get('button[aria-label="Add block in position 2"]').click(); + cy.get('.blocks-chooser [aria-label="Unfold Text blocks"]').click(); + cy.wait(200); + cy.get('.blocks-chooser .text .button.slate').click(); + cy.getSlateEditorSelectorAndType( + '.block.gridBlock.selected .slate-editor [contenteditable=true]', + 'Hello world', + ); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + + cy.get('#page-add .block.gridBlock .block.image figcaption .title', { + timeout: 10000, + }).should('contain', 'Hallo'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.gridBlock h2.headline').should('contain', 'Hallo'); + cy.get('.block.gridBlock .block.image figcaption .title').should( + 'contain', + 'Hallo', + ); + cy.get('.block.gridBlock .slate').should('contain', 'Hallo'); + cy.get('.block.gridBlock .block.image a') + .should('have.attr', 'href') + .and('include', '/de'); + cy.get('.block.gridBlock .block.teaser a') + .should('have.attr', 'href') + .and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/headingBlock.cy.js b/frontend/cypress/tests/deepl/headingBlock.cy.js new file mode 100644 index 00000000..9d385184 --- /dev/null +++ b/frontend/cypress/tests/deepl/headingBlock.cy.js @@ -0,0 +1,43 @@ +context('Heading Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a heading block and translate it', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('heading'); + cy.get('.block.heading .editable').click().type('Hello world'); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + cy.get('.block.heading .heading').should('contain', 'Hello world'); + + // translate the page to German + cy.createTranslation(); + + cy.get('#page-add .block.heading .heading', { timeout: 10000 }).should( + 'contain', + 'Hallo', + ); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.heading .heading').should('contain', 'Hallo'); + }); +}); diff --git a/frontend/cypress/tests/deepl/highlightBlock.cy.js b/frontend/cypress/tests/deepl/highlightBlock.cy.js new file mode 100644 index 00000000..8c1f78a2 --- /dev/null +++ b/frontend/cypress/tests/deepl/highlightBlock.cy.js @@ -0,0 +1,61 @@ +context('Highlight Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a highlight block (title, description, buttonText) and translate it', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('Highlight'); + + cy.get('.block.highlight input[type="file"]').attachFile( + 'halfdome2022.jpg', + { + subjectType: 'input', + encoding: 'utf8', + }, + ); + cy.get('.block.highlight .highlight-image-wrapper img').should('exist'); + + cy.get('.block.highlight .title [contenteditable=true]') + .click() + .type('Hello'); + cy.getSlateEditorSelectorAndType( + '.block.highlight .description [contenteditable=true]', + 'Hello world', + ); + + cy.get('.ui.checkbox:last').click({ force: true }); + cy.get('#field-buttonText').type('Read more'); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + + cy.get('#page-add .block.highlight .title', { timeout: 10000 }).should( + 'contain', + 'Hallo', + ); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.highlight .title').should('contain', 'Hallo'); + cy.get('.block.highlight .description').should('contain', 'Hallo'); + }); +}); diff --git a/frontend/cypress/tests/deepl/imageBlock.cy.js b/frontend/cypress/tests/deepl/imageBlock.cy.js new file mode 100644 index 00000000..82540b46 --- /dev/null +++ b/frontend/cypress/tests/deepl/imageBlock.cy.js @@ -0,0 +1,81 @@ +context('Image Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-link-target', + contentTitle: 'Link Target', + path: '/en', + }); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create /en page with an image block and translate it', () => { + cy.visit('/en/my-link-target'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.get('.ui.basic.icon.button.block-add-button').click(); + cy.get('.ui.basic.icon.button.image').contains('Image').click(); + cy.get('input[type="file"]').attachFile('halfdome2022.jpg', { + subjectType: 'input', + encoding: 'utf8', + }); + cy.get('.block.image figure img').should('exist'); + cy.get('.field-wrapper-alt #field-alt').type('Volto Logo'); + cy.get( + '#blockform-fieldset-default .field-wrapper-title #field-title', + ).type('Hello'); + cy.get( + '#blockform-fieldset-default .field-wrapper-description #field-description', + ).type('Hello world'); + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Link Target'); + cy.wait(500); + cy.get('[aria-label="Select Link Target"]').dblclick(); + cy.get('#toolbar-save').click(); + + cy.get('.block.image figcaption .title').should('contain', 'Hello'); + cy.get('.block.image figcaption .description').should( + 'contain', + 'Hello world', + ); + cy.get('.block.image a') + .should('have.attr', 'href') + .and('include', '/en/my-link-target'); + + cy.createTranslation(); + + cy.get('#page-add .block.image figcaption .title', { + timeout: 10000, + }).should('contain', 'Hallo'); + cy.get('#page-add .block.image figcaption .description').should( + 'contain', + 'Hallo', + ); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + cy.get('.block.image a').should('have.attr', 'href').and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/introductionBlock.cy.js b/frontend/cypress/tests/deepl/introductionBlock.cy.js new file mode 100644 index 00000000..568c3634 --- /dev/null +++ b/frontend/cypress/tests/deepl/introductionBlock.cy.js @@ -0,0 +1,44 @@ +context('Introduction Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create an introduction block and translate it', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('introduction'); + cy.getSlateEditorSelectorAndType( + '.block.introduction .slate-editor [contenteditable=true]', + 'Hello world', + ); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + + cy.get('#page-add .block.introduction', { timeout: 10000 }).should( + 'contain', + 'Hallo', + ); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.introduction').should('contain', 'Hallo'); + }); +}); diff --git a/frontend/cypress/tests/deepl/listingBlock.cy.js b/frontend/cypress/tests/deepl/listingBlock.cy.js new file mode 100644 index 00000000..56c992a2 --- /dev/null +++ b/frontend/cypress/tests/deepl/listingBlock.cy.js @@ -0,0 +1,77 @@ +describe('Listing Block Tests', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'My Page', + path: '/en/', + }); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-page-test', + contentTitle: 'My Page Test', + path: '/en/', + }); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-folder', + contentTitle: 'My Folder', + path: '/en/', + }); + }); + + it('listed items and the listing headline are translated', () => { + ['my-page-test', 'my-folder'].forEach((id) => { + cy.visit(`/en/${id}`); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de'); + }); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + + cy.clearSlateTitle().type('My title'); + cy.addNewBlock('listing'); + cy.get('#field-headline').type('Hello'); + + cy.configureListingWith('Page'); + + cy.get('#select-listingblock-sort-on') + .click() + .type('Effective date {enter}'); + cy.get('input[name="field-sort_order_boolean-2-querystring"]') + .check({ force: true }) + .should('be.checked'); + + cy.get('#toolbar-save').click(); + cy.wait('@content'); + + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/mein-titel'); + cy.get('#page-document .block.listing .headline').should( + 'contain', + 'Hallo', + ); + + cy.visit('/de/mein-titel'); + cy.wait('@content'); + cy.get('#page-document .block.listing .listing-item a') + .should('have.length.greaterThan', 0) + .each(($a) => { + const href = $a.attr('href'); + expect(href).to.include('/de'); + }); + }); +}); diff --git a/frontend/cypress/tests/deepl/logosBlock.cy.js b/frontend/cypress/tests/deepl/logosBlock.cy.js new file mode 100644 index 00000000..def8353b --- /dev/null +++ b/frontend/cypress/tests/deepl/logosBlock.cy.js @@ -0,0 +1,103 @@ +context('Logos Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + cy.viewport('macbook-16'); + + cy.setupDeepL(); + + // The intranet is a multilingual site: all content lives under `/en`. + cy.fixture('halfdome2022.jpg', 'base64').then((fileContent) => { + cy.createContent({ + contentType: 'Image', + contentId: 'my-image', + contentTitle: 'My Image', + path: '/en', + bodyModifier(body) { + body.image = { + data: fileContent, + encoding: 'base64', + filename: 'image.png', + 'content-type': 'image/png', + }; + return body; + }, + }); + }); + + // The Document the logo links to. + cy.createContent({ + contentType: 'Document', + contentId: 'blue-orchids', + contentTitle: 'Blue Orchids', + path: '/en', + }); + + // The Document that hosts the logos block. + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a logos block (alt + href) and translate it', () => { + // Translate the link target first so the logo href is rewritten on translation. + cy.visit('/en/blue-orchids'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('logos'); + + // Add a logo item. + cy.get( + '#sidebar-properties .field-wrapper-logos .add-item-button-wrapper button[aria-label="Add logo"]', + ).click(); + + // Logo image (object browser opens on the page folder, use site-wide search). + cy.get( + '.olw-item-wrapper.active .objectbrowser-field[aria-labelledby^="fieldset-default-field-label-logo-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('My Image'); + cy.wait(500); + cy.get('[aria-label="Select My Image"]').dblclick(); + + // Alt text (translatable). + cy.get('.olw-item-wrapper.active [id^="field-alt-"]').type('Hello world'); + + // Link target (translatable href). + cy.get( + '.olw-item-wrapper.active .objectbrowser-field[aria-labelledby^="fieldset-default-field-label-href-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Blue Orchids'); + cy.wait(500); + cy.get('[aria-label="Select Blue Orchids"]').dblclick(); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + cy.wait(2000); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.logos img.logo-image') + .should('have.attr', 'alt') + .and('include', 'Hallo'); + cy.get('.block.logos a.logo-link') + .should('have.attr', 'href') + .and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/mapsBlock.cy.js b/frontend/cypress/tests/deepl/mapsBlock.cy.js new file mode 100644 index 00000000..e504cb0f --- /dev/null +++ b/frontend/cypress/tests/deepl/mapsBlock.cy.js @@ -0,0 +1,45 @@ +context('Maps Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a maps block with a title and translate it', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('maps'); + cy.get('.block.maps .toolbar-inner .ui.input input') + .type( + '', + ) + .type('{enter}'); + + // The maps title is rendered as the iframe `title` attribute. + cy.get('#sidebar-properties #field-title').type('Hello world'); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + cy.createTranslation(); + cy.wait(2000); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.maps iframe.google-map') + .should('have.attr', 'title') + .and('include', 'Hallo'); + }); +}); diff --git a/frontend/cypress/tests/deepl/searchBlock.cy.js b/frontend/cypress/tests/deepl/searchBlock.cy.js new file mode 100644 index 00000000..a6132e0a --- /dev/null +++ b/frontend/cypress/tests/deepl/searchBlock.cy.js @@ -0,0 +1,41 @@ +context('Search Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a search block with a headline and translate it', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('search'); + + cy.get('#sidebar-properties #field-headline').type('Hello world'); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + + cy.get('#page-add .searchBlock-container .headline', { + timeout: 10000, + }).should('contain', 'Hallo'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.searchBlock-container .headline').should('contain', 'Hallo'); + }); +}); diff --git a/frontend/cypress/tests/deepl/slateBlock.cy.js b/frontend/cypress/tests/deepl/slateBlock.cy.js new file mode 100644 index 00000000..4ceacafc --- /dev/null +++ b/frontend/cypress/tests/deepl/slateBlock.cy.js @@ -0,0 +1,28 @@ +context('Slate Block', () => { + beforeEach(() => { + cy.setupDeepL(); + }); + + it('create /en page and translate it', () => { + cy.visit('/en', { + headers: { + Accept: 'application/json', + }, + failOnStatusCode: false, + retryOnNetworkFailure: true, + }); + cy.url().should('include', '/en'); + + cy.get('#toolbar-add').click(); + cy.get('#toolbar-add-document').click(); + cy.get('.documentFirstHeading').type('Hello'); + cy.get('.text-slate-editor-inner').type('Hello world'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/hello'); + + cy.createTranslation(); + cy.get('#page-add h1', { timeout: 10000 }).should('contain', 'Hallo'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/hallo'); + }); +}); diff --git a/frontend/cypress/tests/deepl/sliderBlock.cy.js b/frontend/cypress/tests/deepl/sliderBlock.cy.js new file mode 100644 index 00000000..2cf275cc --- /dev/null +++ b/frontend/cypress/tests/deepl/sliderBlock.cy.js @@ -0,0 +1,109 @@ +context('Slider Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + cy.viewport('macbook-16'); + + cy.setupDeepL(); + + cy.fixture('halfdome2022.jpg', 'base64').then((fileContent) => { + cy.createContent({ + contentType: 'Image', + contentId: 'my-image', + contentTitle: 'My Image', + path: '/en', + bodyModifier(body) { + body.image = { + data: fileContent, + encoding: 'base64', + filename: 'image.png', + 'content-type': 'image/png', + }; + return body; + }, + }); + }); + + cy.createContent({ + contentType: 'Document', + contentId: 'blue-orchids', + contentTitle: 'Blue Orchids', + contentDescription: 'are growing on the mountain tops', + preview_image_link: { + '@id': '/en/my-image', + }, + path: '/en', + }); + + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Document', + path: '/en', + }); + }); + + it('add a default variant slider block and translate it', () => { + cy.visit('/en/blue-orchids'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.get('.block .slate-editor [contenteditable=true]').click(); + cy.get('.button .block-add-button').click({ force: true }); + cy.get('.blocks-chooser .mostUsed .button.slider') + .contains('Slider') + .click({ force: true }); + + cy.get( + '.objectbrowser-field[aria-labelledby^="fieldset-default-field-label-href-0-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Blue Orchids'); + cy.wait(500); + cy.get('[aria-label="Select Blue Orchids"]').dblclick(); + + cy.get('[id^="field-head_title-1-"]') + .click() + .clear() + .type('Hello from Germany'); + cy.get('[id^="field-title-2-"]').click().clear().type('Hello title'); + cy.get('[id^="field-description-3-"]').click().clear().type('Hello world'); + cy.get('[id^="field-buttonText-5-"]').click().clear().type('Read More'); + + cy.get( + '.objectbrowser-field[aria-labelledby^="fieldset-default-field-label-preview_image-4-"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('My Image'); + cy.wait(500); + cy.get('[aria-label="Select My Image"]').dblclick(); + cy.wait(500); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + cy.get('#page-add .block.slider .title h2', { timeout: 10000 }).should( + 'contain', + 'Hallo', + ); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.slider .title h2').should('contain', 'Hallo'); + cy.get('.block.slider p').should('contain', 'Hallo'); + cy.get('.block.slider .highlight-image-wrapper img').should('be.visible'); + cy.get('.block.slider a') + .first() + .should('have.attr', 'href') + .and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/tableBlock.cy.js b/frontend/cypress/tests/deepl/tableBlock.cy.js new file mode 100644 index 00000000..1792e18e --- /dev/null +++ b/frontend/cypress/tests/deepl/tableBlock.cy.js @@ -0,0 +1,88 @@ +context('Table Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.on('uncaught:exception', (err) => { + if (err.message.includes("Failed to execute 'removeChild'")) { + return false; + } + return true; + }); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a table block and translate its cells', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('table'); + cy.wait(2000); + + // Fill every cell of the default 2x2 table. + cy.get( + '.celled.fixed.table thead tr th:first-child() [contenteditable="true"]', + ) + .focus() + .click() + .type('Hello world one'); + cy.get( + '.celled.fixed.table thead tr th:nth-child(2) [contenteditable="true"]', + ) + .focus() + .click() + .type('Hello world two'); + cy.get( + '.celled.fixed.table tbody tr:nth-child(1) td:first-child() [contenteditable="true"]', + ) + .focus() + .click() + .type('Hello world three'); + cy.get( + '.celled.fixed.table tbody tr:nth-child(1) td:nth-child(2) [contenteditable="true"]', + ) + .focus() + .click() + .type('Hello world four'); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + // translate the page to German + cy.createTranslation(); + + cy.get('#page-add .celled.fixed.table thead tr th:first-child()', { + timeout: 10000, + }).should('contain', 'Hallo'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.celled.fixed.table thead tr th:first-child()').should( + 'contain', + 'Hallo', + ); + cy.get('.celled.fixed.table thead tr th:nth-child(2)').should( + 'contain', + 'Hallo', + ); + cy.get('.celled.fixed.table tbody tr:nth-child(1) td:first-child()').should( + 'contain', + 'Hallo', + ); + cy.get('.celled.fixed.table tbody tr:nth-child(1) td:nth-child(2)').should( + 'contain', + 'Hallo', + ); + }); +}); diff --git a/frontend/cypress/tests/deepl/teaserBlock.cy.js b/frontend/cypress/tests/deepl/teaserBlock.cy.js new file mode 100644 index 00000000..5b0b9404 --- /dev/null +++ b/frontend/cypress/tests/deepl/teaserBlock.cy.js @@ -0,0 +1,75 @@ +context('Teaser Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-link-target', + contentTitle: 'Link Target', + path: '/en', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a teaser block with customised fields and translate it', () => { + cy.visit('/en/my-link-target'); + cy.createTranslation(); + cy.url().should('include', '/de'); + cy.get('#toolbar-save').click(); + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.addNewBlock('teaser'); + cy.get( + '.objectbrowser-field[aria-labelledby="fieldset-default-field-label-href"] button[aria-label="Open object browser"]', + ).click(); + cy.findByLabelText('Search SVG').click(); + cy.get('.ui.input.search').type('Link Target'); + cy.wait(500); + cy.get('[aria-label="Select Link Target"]').dblclick(); + cy.get('#field-overwrite').check({ force: true }); + cy.get( + '#blockform-fieldset-default .field-wrapper-title #field-title', + ).type('Hello'); + cy.get( + '#blockform-fieldset-default .field-wrapper-head_title #field-head_title', + ).type('Hello'); + cy.get( + '#blockform-fieldset-default .field-wrapper-description #field-description', + ).type('Hello world'); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + cy.get('.block.teaser .headline').should('contain', 'Hello'); + cy.get('.block.teaser .title').should('contain', 'Hello'); + cy.get('.block.teaser .description').should('contain', 'Hello world'); + cy.get('.block.teaser a') + .should('have.attr', 'href') + .and('include', '/en/my-link-target'); + + cy.createTranslation(); + + cy.get('#page-add .block.teaser .title', { timeout: 10000 }).should( + 'contain', + 'Hallo', + ); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.block.teaser .headline').should('contain', 'Hallo'); + cy.get('.block.teaser .title').should('contain', 'Hallo'); + cy.get('.block.teaser .description').should('contain', 'Hallo'); + cy.get('.block.teaser a').should('have.attr', 'href').and('include', '/de'); + }); +}); diff --git a/frontend/cypress/tests/deepl/tocBlock.cy.js b/frontend/cypress/tests/deepl/tocBlock.cy.js new file mode 100644 index 00000000..b261fe84 --- /dev/null +++ b/frontend/cypress/tests/deepl/tocBlock.cy.js @@ -0,0 +1,45 @@ +context('Table of Contents Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a toc block with a title and translate it', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click().type('## Hello heading{enter}'); + cy.getSlate().click().type('### Hello world subheading{enter}'); + + cy.getSlate().click().type('/table{downarrow}{enter}'); + + cy.get('#blockform-fieldset-default #field-title').type('Hello world'); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + cy.createTranslation(); + + cy.get('#page-add .table-of-contents h2', { timeout: 10000 }).should( + 'contain', + 'Hallo', + ); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + + cy.get('.table-of-contents h2').should('contain', 'Hallo'); + cy.get('.table-of-contents li').should('contain', 'Hallo'); + cy.get('.table-of-contents li ul li').should('contain', 'Hallo'); + }); +}); diff --git a/frontend/cypress/tests/deepl/videoBlock.cy.js b/frontend/cypress/tests/deepl/videoBlock.cy.js new file mode 100644 index 00000000..af690a40 --- /dev/null +++ b/frontend/cypress/tests/deepl/videoBlock.cy.js @@ -0,0 +1,41 @@ +context('Video Block', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + + cy.setupDeepL(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'Hello', + path: '/en', + }); + }); + + it('create a video block with a title and translate it', () => { + cy.visit('/en/my-page'); + cy.wait('@content'); + cy.navigate('/en/my-page/edit'); + cy.wait('@schema'); + cy.url().should('include', '/en'); + + cy.getSlate().click(); + cy.get('.ui.basic.icon.button.block-add-button').click(); + cy.get('.ui.basic.icon.button.video').contains('Video').click(); + cy.get('.toolbar-inner > .ui > input') + .filter(':visible') + .click() + .type('https://youtu.be/T6J3d35oIAY') + .type('{enter}'); + + cy.get('#sidebar-properties #field-title').type('Hello world'); + + cy.get('#toolbar-save').click(); + cy.url().should('include', '/en/my-page'); + + cy.createTranslation(); + cy.wait(2000); + cy.get('#toolbar-save').click(); + cy.url().should('include', '/de/'); + }); +}); diff --git a/frontend/mrs.developer.json b/frontend/mrs.developer.json index 4a02ff51..25ee8411 100644 --- a/frontend/mrs.developer.json +++ b/frontend/mrs.developer.json @@ -144,5 +144,13 @@ "url": "git@github.com:kitconcept/volto-contact-block.git", "https": "https://github.com/kitconcept/volto-contact-block.git", "tag": "1.0.0a6" + }, + "volto-kitconcept-deepl": { + "develop": true, + "output": "packages", + "package": "@kitconcept/volto-kitconcept-deepl", + "url": "git@github.com:kitconcept/kitconcept-deepl.git", + "https": "https://github.com/kitconcept/kitconcept-deepl.git", + "branch": "main" } } diff --git a/frontend/package.json b/frontend/package.json index 225aa572..8cc3ca97 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -57,6 +57,7 @@ "@kitconcept/volto-carousel-block": "workspace:*", "@kitconcept/volto-iframe-block": "workspace:*", "@kitconcept/volto-bm3-compat": "workspace:*", + "@kitconcept/volto-kitconcept-deepl": "workspace:*", "react-aria-components": "catalog:" }, "ignoredBuiltDependencies": [ @@ -74,4 +75,4 @@ ] }, "packageManager": "pnpm@10.20.0" -} \ No newline at end of file +} diff --git a/frontend/packages/kitconcept-intranet/news/+addCypressTestForBlocksDeepL.internal b/frontend/packages/kitconcept-intranet/news/+addCypressTestForBlocksDeepL.internal new file mode 100644 index 00000000..c25e402a --- /dev/null +++ b/frontend/packages/kitconcept-intranet/news/+addCypressTestForBlocksDeepL.internal @@ -0,0 +1 @@ +Add Cypress tests for blocks translating using DeepL @Tishasoumya-02 \ No newline at end of file diff --git a/frontend/packages/kitconcept-intranet/package.json b/frontend/packages/kitconcept-intranet/package.json index e866739f..89ab4b09 100644 --- a/frontend/packages/kitconcept-intranet/package.json +++ b/frontend/packages/kitconcept-intranet/package.json @@ -65,7 +65,8 @@ "@kitconcept/core", "@plone-collective/volto-authomatic", "@kitconcept/volto-solr", - "@kitconcept/volto-contact-block" + "@kitconcept/volto-contact-block", + "@kitconcept/volto-kitconcept-deepl" ], "dependencies": { "@eeacms/volto-accordion-block": "^12.0.0", @@ -90,6 +91,7 @@ "@plone/volto-form-block": "^1.0.0-alpha.0", "@plone-collective/volto-authomatic": "3.0.0-alpha.6", "@plone-collective/volto-image-editor": "1.0.1", + "@kitconcept/volto-kitconcept-deepl": "workspace:*", "classnames": "2.5.1", "lodash": "4.17.21", "react-aria-components": "catalog:", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index f0667b27..a2458ee6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -113,6 +113,7 @@ overrides: '@kitconcept/volto-carousel-block': workspace:* '@kitconcept/volto-iframe-block': workspace:* '@kitconcept/volto-bm3-compat': workspace:* + '@kitconcept/volto-kitconcept-deepl': workspace:* react-aria-components: ^1.17.0 pnpmfileChecksum: sha256-XPzenrDMYKqaPYJjupNoENqK/BtpNsTwr2QxrHfmU4A= @@ -1442,6 +1443,9 @@ importers: '@kitconcept/volto-introduction-block': specifier: workspace:* version: link:../volto-introduction-block/packages/volto-introduction-block + '@kitconcept/volto-kitconcept-deepl': + specifier: workspace:* + version: link:../volto-kitconcept-deepl/frontend/packages/volto-kitconcept-deepl '@kitconcept/volto-light-theme': specifier: workspace:* version: link:../volto-light-theme/frontend/packages/volto-light-theme @@ -1859,6 +1863,43 @@ importers: specifier: ^3.2.4 version: 3.2.6(@types/debug@4.1.13)(@types/node@24.13.2)(@vitest/ui@3.2.6)(jiti@2.7.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@3.13.1)(lightningcss@1.32.0)(sass@1.58.0)(terser@5.48.0)(yaml@2.9.0) + packages/volto-kitconcept-deepl/frontend/packages/volto-kitconcept-deepl: + dependencies: + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@plone/registry': + specifier: workspace:* + version: link:../../../../../core/packages/registry + '@plone/scripts': + specifier: workspace:* + version: link:../../../../../core/packages/scripts + '@plone/types': + specifier: workspace:* + version: link:../../../../../core/packages/types + '@testing-library/react': + specifier: ^16.2.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@types/react': + specifier: ^18.3.1 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.31) + release-it: + specifier: ^19.0.5 + version: 19.2.4(@types/node@24.13.1)(magicast@0.3.5) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^3.1.2 + version: 3.2.6(@types/debug@4.1.13)(@types/node@24.13.1)(@vitest/ui@3.2.6)(jiti@2.7.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@3.13.1)(lightningcss@1.32.0)(sass@1.58.0)(terser@5.48.0)(yaml@2.9.0) + packages/volto-light-theme/frontend/packages/volto-light-theme: dependencies: '@dnd-kit/core':