diff --git a/.github/workflows/cypress-Run-tests-with-npm-packages.yml b/.github/workflows/cypress-Run-tests-with-npm-packages.yml
deleted file mode 100644
index ae600ce55..000000000
--- a/.github/workflows/cypress-Run-tests-with-npm-packages.yml
+++ /dev/null
@@ -1,146 +0,0 @@
-# It runs all available Cypress tests and sends test data to Cypress Dashboard.
-# It executes on every Monday at 01 a.m, and on demand by the user.
-# The tests run with a fail-fast strategy, once one fails, the others will not run.
-# If a test fails, it creates a set of screenshots showing the errors.
-#
-# Matrix:
-# - Browser: chrome.
-# - Test folder: All tests available on the `/tests` folder.
-#
-# Jobs:
-# - Set up the Cypress Environment: `setup-cypress`.
-# - Runs all tests on the matrix: `run-all-tests-matrix`.
-
-name: Run Cypress tests with npm packages
-
-on:
- schedule:
- # runs every Monday at 1 a.m
- - cron: "0 1 * * 1"
- filters:
- branches:
- only:
- - stable
- workflow_dispatch:
- inputs:
- # Sends data to Cy
- cypress-record:
- description: Send test data to Cypress Dashboard? Write 'Yes' to send data.
- required: true
- default: "No"
-
-jobs:
- setup-cypress:
- # Setup Cypress environment without cache.
- name: Setup Cypress environment without cache
- runs-on: ubuntu-latest
- steps:
- # 01. Checkout the repository.
- - name: Checkout
- uses: actions/checkout@v2
-
- # 02. Install a specific version of Node using.
- - name: Use Node.js
- uses: actions/setup-node@v2
- with:
- node-version: 16
-
- # 03. Install dependencies and verify Cypress
- - name: Install dependencies and verify Cypress
- env:
- # make sure every Cypress install prints minimal information
- CI: 1
- # print Cypress and OS info
- # This next command should use "npm ci" instead of "npm install"
- run: |
- npm ci
- npx cypress verify
- npx cypress info
- npx cypress version
- npx cypress version --component package
- npx cypress version --component binary
- npx cypress version --component electron
- npx cypress version --component node
-
- run-all-tests-matrix:
- # Runs all tests.
- runs-on: ubuntu-latest
- needs: setup-cypress
- strategy:
- fail-fast: true
- matrix:
- # Define values for browsers from
- browser: ["chrome"]
- # browser: ['chrome', 'edge', 'firefox', 'chromium']
- type: ["all"]
- # type: ['smoke','e2e', 'ui', 'validation']
- # env: ['local', 'public']
- name: Run ${{ matrix.type }} tests on ${{ matrix.browser }}
- steps:
- # 01. Checkout the repository.
- - name: Checkout
- uses: actions/checkout@v2
-
- # 02. Install a specific version of Node.
- - name: Use Node.js
- uses: actions/setup-node@v2
- with:
- node-version: 16
-
- # 03a. Decide whether to send data to Cypress Dashboard, or not, for workflow_dispatch and schedule.
- - name: Decide if we send data to Cypress 2
- if: ${{ github.event.inputs.cypress-record == 'Yes' || github.event_name == 'schedule' }}
- run: |
- echo "CY_RECORD_KEY=${{ secrets.CYPRESS_RECORD_KEY }}" >> $GITHUB_ENV
- echo "CY_RECORD_FLAG=-- --record --key " >> $GITHUB_ENV
-
- # 03a. Decide whether to send data to Cypress Dashboard, or not, for workflow_dispatch.
- - name: Decide if we send data to Cypress 1
- # We don't want to send the tests to Dashboard through workflow dispatch
- if: ${{ github.event.inputs.cypress-record != 'Yes' }}
- # we set the environment variables dynamically to empty in order to avoid
- # recording the test execution to Cypress Dashboard.
- run: |
- echo "CY_RECORD_KEY=" >> $GITHUB_ENV
- echo "CY_RECORD_FLAG=" >> $GITHUB_ENV
-
- # 04. Run the tests following the initial matrix: by browser and test type
- - name: Run tests by browser
- uses: cypress-io/github-action@v2
- timeout-minutes: 10
- with:
- # 'build' starts the default demo
- build: npm run build
- # 'test:ci' runs tests over Docker image for build context
- # we also send the Cypress Dashboard record key dynamically
- command: npm run test:ci ${{ env.CY_RECORD_FLAG }} ${{ env.CY_RECORD_KEY }}
- record: true
- parallel: true
- group: "${{ matrix.type }} tests on ${{ matrix.browser }}"
- browser: ${{ matrix.browser }}
- config: "video: true"
- spec: |
- cypress/tests/**/*.js
- env:
- # https://github.com/wiris/html-integrations/settings/secrets/actions
- CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
- GITHUB_TOKEN: ${{ secrets.GH_CICD_TOKEN }}
-
- # 05a. Save videos and screenshots as test artifacts
- # https://github.com/actions/upload-artifact
- - name: Upload screenshots
- uses: actions/upload-artifact@master
- # there might be no screenshots created when:
- # - there are no test failures
- # so only upload screenshots if previous step has failed
- if: failure()
- with:
- name: screenshots-${{ matrix.type }}-${{ matrix.browser }}
- path: cypress/screenshots
-
- # 05b. Upload videos, since they are always be generated.
- - name: Upload videos for all tests
- uses: actions/upload-artifact@master
- with:
- name: videos-${{ matrix.type }}-${{ matrix.browser }}
- path: cypress/videos
diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml
new file mode 100644
index 000000000..fd739cff2
--- /dev/null
+++ b/.github/workflows/run-e2e-tests.yml
@@ -0,0 +1,108 @@
+name: E2E Tests - All Editors
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ # Matrix strategy to enable per html editor parallelization
+ e2e-tests:
+ name: E2E Tests - ${{ matrix.editor }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ editor:
+ - generic
+ - ckeditor4
+ - ckeditor5
+ - froala
+ - tinymce5
+ - tinymce6
+ - tinymce7
+ - tinymce8
+
+ steps:
+ - uses: actions/checkout@v5
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "23.x"
+
+ - name: Install dependencies
+ run: |
+ yarn install
+ yarn playwright install --with-deps
+
+ - name: Build ${{ matrix.editor }} package
+ if: matrix.editor != 'tinymce8'
+ run: |
+ yarn nx build ${{ matrix.editor }}
+ yarn nx build html-${{ matrix.editor }}
+ env:
+ CKEDITOR4_API_KEY: ${{ secrets.CKEDITOR4_API_KEY }}
+ FROALA_API_KEY: ${{ secrets.FROALA_API_KEY }}
+
+ - name: Build tinymce8 packages
+ if: matrix.editor == 'tinymce8'
+ run: |
+ yarn nx build tinymce7
+ yarn nx build html-tinymce8
+
+ - name: Run E2E tests for ${{ matrix.editor }}
+ id: e2e
+ run: HTML_EDITOR=${{ matrix.editor }} PLAYWRIGHT_BLOB_OUTPUT_NAME=report-${{ matrix.editor }}.zip yarn test:e2e
+ continue-on-error: true
+ env:
+ CKEDITOR4_API_KEY: ${{ secrets.CKEDITOR4_API_KEY }}
+ FROALA_API_KEY: ${{ secrets.FROALA_API_KEY }}
+
+ - name: Publish test results for ${{ matrix.editor }}
+ uses: dorny/test-reporter@d61b558e8df85cb60d09ca3e5b09653b4477cea7 # v2.0.0
+ with:
+ name: E2E Tests - ${{ matrix.editor }}
+ path: tests/e2e/test-results/results.xml
+ reporter: java-junit
+ fail-on-error: ${{ steps.e2e.outcome == 'failure' }}
+ continue-on-error: true
+
+ - name: Upload blob report for ${{ matrix.editor }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: blob-report-${{ matrix.editor }}
+ path: blob-report
+ retention-days: 1
+
+ merge-reports:
+ if: always()
+ needs: [e2e-tests]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "23.x"
+
+ - name: Install dependencies
+ run: yarn install
+
+ - name: Download all blobs
+ uses: actions/download-artifact@v4
+ with:
+ path: all-blob-reports
+ pattern: blob-report-*
+ merge-multiple: true
+
+ - name: Merge into HTML Report
+ run: npx playwright merge-reports --reporter html ./all-blob-reports
+
+ - name: Upload Final HTML Report
+ uses: actions/upload-artifact@v4
+ with:
+ name: final-playwright-report
+ path: playwright-report/
+ retention-days: 2
diff --git a/.gitignore b/.gitignore
index e2b72db8e..0576f28bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,10 +7,6 @@
package-lock.json
packages/*/yarn.lock
-# Cypress
-cypress/screenshots
-cypress/videos
-
node_modules
# Verdaccio
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index ce541f0a9..00c201f60 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -20,6 +20,7 @@
"ms-vsliveshare.vsliveshare", // Live Share
"visualstudioexptteam.vscodeintellicode", // IntelliCode
"thundergang.thunder-client", // Thunder Client
+ "ms-playwright.playwright", // Playwright
// GITHUB
"eamodio.gitlens", // GitLens
diff --git a/cypress.json b/cypress.json
deleted file mode 100644
index ea606fa3b..000000000
--- a/cypress.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "integrationFolder": "cypress/tests",
- "screenshotOnRunFailure": true,
- "env": {
- "FAIL_FAST_STRATEGY": "run",
- "FAIL_FAST_ENABLED": true
- }
-}
diff --git a/cypress/README.md b/cypress/README.md
deleted file mode 100644
index 0291ea446..000000000
--- a/cypress/README.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# Cypress End-to-end tests for the MathType Web Integrations
-
-[Cypress.io](https://www.cypress.io) is an open source, MIT licensed end-to-end test runner.
-
-**Important**: More information about testing commands and how to use instructions on the [Testing section](/docs/development/testing/README.md) of this project documentation.
-
-## Folder structure
-
-These folders hold end-to-end tests and supporting files for the Cypress Test Runner.
-
-- [fixtures](fixtures) holds sample data for mocking our tests, [read more](https://on.cypress.io/fixture).
-- [tests](tests) holds the actual integration test files, [read more](https://on.cypress.io/writing-and-organizing-tests)
-- [plugins](plugins) allow you to customize how tests are loaded, [read more](https://on.cypress.io/plugins)
-- [support](support) includes our custom commands, [read more](https://on.cypress.io/writing-and-organizing-tests#Support-file)
-
-## What do we want to test?
-
-A minimal MathType Integration for the web that includes mathematical formula editing & rendering features from Wiris.
-
-### Elements
-
-It would consist on these next HTML elements:
-
-1. an editable element, with a default value or not
-2. a read-only element with its content synchronized to the previous element through an `onChange` event
-3. the MathType and ChemType buttons, over the editable content
-
-Also, whenever the MathType or ChemType buttons are clicked, or a mathematical expression inside the textarea is clicked,
-
-4. a MathType Modal Window is shown to the user to edit the formula.
-
-### Source code
-
-A canonical representation of the HTML source code of this app would look like this:
-
-```html
-
-
-
-
-
-
...
-
-
-```
-
-### UI Preview
-
-This next diagram represents a common E2E interaction with the MathType for the Web UI elements of the canonical MathType Integration sample app: adding a mathematical formula from scratch using the MathType editor.
-
-
-
-### MathType Modal Window
-
-The MathType Modal Window consists on the following elements:
-
-
-
-> This diagram is based on a comment in the source code of [`modal.js`](/packages/mathtype-html-integration-devkit/src/modal.js) from the `mathtype-html-integration-devkit` package.
-
-## `cypress.json` file
-
-You can configure project options in the [../cypress.json](../cypress.json) file, see [Cypress configuration doc](https://on.cypress.io/configuration).
-
-The current values we've set by default for all environments are:
-
-```json
- // By default, point to the demos/html/generic App.
- "baseUrl": "http://localhost:8007",
- // Override default 'integration' value to 'tests'
- "integrationFolder": "cypress/tests",
- "screenshotOnRunFailure": true,
- // Optimize test execution by activating Fail_fast feature everywhere.
- "env":
- {
- "FAIL_FAST_STRATEGY": "run",
- "FAIL_FAST_ENABLED": true
- }
-
-```
-
-The main cypress.json files will hold the default settings for all tests in all environments: local, build, ...
-
-## More information
-
-- [https://github.com/cypress.io/cypress](https://github.com/cypress.io/cypress)
-- [https://docs.cypress.io/](https://docs.cypress.io/)
-- [Writing your first Cypress test](http://on.cypress.io/intro)
diff --git a/cypress/YYY-ZZZ.category.js b/cypress/YYY-ZZZ.category.js
deleted file mode 100644
index aca165ed6..000000000
--- a/cypress/YYY-ZZZ.category.js
+++ /dev/null
@@ -1,32 +0,0 @@
-///
-// ***********************************************************
-// Test case: {Test.ID} Ex. INT-STD-014
-// Title: {Test.Title} Ex. User creates a new formula from scratch using MathType.
-// Document: {Test.URL} Ex. https://docs.google.com/document/d/1fiGsUwqNIsjiaJI0aGfH_aNX5OJKEHkfWtfvlQkEEFI/edit
-// Context: {Test.Type} - {Text.category} Ex. UI - Formula insertion/edition
-// Issue: {Test.Issue} Ex. KB-99999
-// ***********************************************************
-
-describe("Formula insertion/edition", () => {
- beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
- // and visit page.
- cy.visit("/");
- // Eventually, clear the editor content: by default the editor could include a mathematical expression.
- cy.getTextEditor().clear();
- });
-
- it("User creates a new formula from scratch using MathType", function () {
- // Open a new MathType modal window clicking the button
- cy.clickButtonToOpenModal();
- // then type a general formula inside the editor
- cy.typeInModal(this.formulas["formula-general"]);
- // and insert the formula at the beginning of the target element using the 'Insert' button.
- cy.clickModalButton("insert");
-
- // Check the recently inserted formula
- // and validate is rendered succesfully using MathType services.
- cy.getFormula(0).isRendered();
- });
-});
diff --git a/cypress/fixtures/formulas.json b/cypress/fixtures/formulas.json
deleted file mode 100644
index 8623cf382..000000000
--- a/cypress/fixtures/formulas.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "formula-general": "x + y",
- "formula-general-alt-en": "x space plus space y",
- "formula-general-alt-es": "x espacio más espacio y",
- "formula-addition": " = z",
- "formula-total-alt-es": "x espacio más espacio y espacio igual espacio z",
- "text-alignment": "2222",
- "formula-alignment": "2+2",
- "latex-general": "$$\\cos^2(x)+\\sin^2(x)$$",
- "latex-general-alt-en": "cos squared left parenthesis x right parenthesis plus sin squared left parenthesis x right parenthesis",
- "latex-addition": "=log(e)",
- "quadratic": "",
- "quadratic-accessible-en": "x equals fraction numerator negative b plus-or-minus square root of b squared minus 4 a c end root over denominator 2 a end fraction",
- "formula-drawn": [
- { "x": 0, "y": 0 },
- { "x": 0, "y": 1 },
- { "x": 1, "y": 0 },
- { "x": 1, "y": 1 }
- ]
-}
diff --git a/cypress/mathtype-web-app.png b/cypress/mathtype-web-app.png
deleted file mode 100644
index b2c4cb2d2..000000000
Binary files a/cypress/mathtype-web-app.png and /dev/null differ
diff --git a/cypress/modal.jpg b/cypress/modal.jpg
deleted file mode 100644
index f49ecee86..000000000
Binary files a/cypress/modal.jpg and /dev/null differ
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
deleted file mode 100644
index 4c63d2db1..000000000
--- a/cypress/plugins/index.js
+++ /dev/null
@@ -1,24 +0,0 @@
-///
-// ***********************************************************
-// This example plugins/index.js can be used to load plugins
-//
-// You can change the location of this file or turn off loading
-// the plugins file with the 'pluginsFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/plugins-guide
-// ***********************************************************
-
-// This function is called when a project is opened or re-opened (e.g. due to
-// the project's config changing)
-
-/**
- * @type {Cypress.PluginConfig}
- */
-module.exports = (on, config) => {
- // `on` is used to hook into various events Cypress emits
- // `config` is the resolved Cypress config
- // eslint-disable-next-line global-require
- require("cypress-fail-fast/plugin")(on, config);
- return config;
-};
diff --git a/cypress/support/commands.d.ts b/cypress/support/commands.d.ts
deleted file mode 100644
index 7a95b4714..000000000
--- a/cypress/support/commands.d.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-///
-
-declare namespace Cypress {
- interface Chainable {
- /**
- * Yield the text editor.
- */
- getTextEditor();
-
- /**
- * Click the MathType or ChemType button in the text editor.
- * @param chem {default = false} whether to click the MathType or ChemType button.
- */
- clickButtonToOpenModal(chem?: boolean);
-
- /**
- * Append the given string in the text editor at the given offset.
- * @param text string to append in the editor
- * @param offset position of the caret when typing. At the end if omitted. Not yet implemented
- */
- typeInTextEditor(text: string, offset?: number);
-
- /**
- * Append the given text in MathType.
- * @param formula formula to append in the editor
- * @param paste whether the text appended is typed or pasted. Not yet implemented
- */
- typeInModal(formula: string, paste?: boolean);
-
- /**
- * Click the specified button on the MathType modal dialog
- * @param {string} button Button identifier. Values can be:
- * - insert: inserts a formula
- * - cancel: closes the modal. If there are changes, opens a confirmation dialog
- * - confirmationClose: discards the changes when closing the modal dialog
- * - confirmationCancel: cancels the confirmation dialog and returns to editing the formula
- * - xClose: closes the modal through the top right x button
- * - maximize: makes the modal full screen through the top right button
- * - stack: changes the modal to not be full screen through the top right button
- * - minimize: hides the modal, if visible, and shows it again, if not visible, through the top right button
- * - hand: opens/closes Hand mode
- */
- clickModalButton(button: string);
-
- /**
- * Insert a formula from scratch
- * @param formula formula to append in the editor
- * @param chem {default = false} whether to click the MathType or ChemType button
- * @param paste {default = false} whether the text appended is typed or pasted. Not yet implemented
- */
- insertFormulaFromScratch(formula: string, chem?: boolean, paste?: boolean);
-
- /**
- * Obtain a formula from a given identifier.
- * @param formulaId identifier of the formula to obtain. The identifier is the 0-indexed position of the formula inside the text editor.
- * @returns the formula
- */
- getFormula(formulaId: number): Chainable;
-
- /**
- * Select a LaTeX formula from a given identifier.
- * @param formulaId id of the formula to obtain. The id is the 0-indexed position of the formula inside the text editor.
- * @returns the formula
- */
- selectLatexFormula(formulaId: number): Chainable;
-
- /**
- * Edit an existing MathType formula by clicking the MathType or ChemType button.
- * Must be applied to a father command unless latex is set to true.
- * @param subject the formula to apply this command to. Not yet implemented
- * @param options object with options:
- * chem {default = false} whether to edit a chem or math formula
- * latex {default = false} whether it is a LaTeX formula or not
- * formulaId id of the formula to edit. Only used when latex is set to true
- * formula string to be added when editing the formula
- */
- editFormula(
- subject: Element,
- formula: string,
- options?: { chem?: boolean; latex?: boolean; formulaId?: number },
- ): Chainable | null;
-
- /**
- * Press the ESC keyboard button
- */
- pressESCButton();
-
- /**
- * Not yet implemented.
- * Drag and drop the MathType or ChemType modal
- * @param coordinates place to drop the modal
- */
- dragDropModal(coordinates: { x: number; y: number });
-
- /**
- * Not yet implemented.
- * Drag and drop a Formula.
- * Must be applied to a father command.
- * @param subject the formula to apply this command to
- * @param coordinates place to drop the formula
- */
- dragDropFormula(subject: Element, coordinates: { x: number; y: number }): Chainable;
-
- /**
- * Not yet implemented.
- * Draw a formula with Hand mode.
- * @param points list of ordered coordinates to draw
- */
- drawFormula(points: { x: number; y: number }[]);
-
- /**
- * Not yet implemented.
- * Resize a given formula.
- * Must be applied to a father command.
- * @param subject the formula to apply this command to
- */
- resizeFormula(subject: Element): Chainable;
- }
-}
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
deleted file mode 100644
index 66cfd3a8b..000000000
--- a/cypress/support/commands.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { createSelection } from "./utils";
-
-Cypress.Commands.add("getTextEditor", () => {
- cy.get("div[contenteditable]");
-});
-
-Cypress.Commands.add("clickButtonToOpenModal", (chem = false) => {
- if (!chem) {
- cy.get("#editorIcon").click();
- } else {
- cy.get("#chemistryIcon").click();
- }
- cy
- // We wait for the toolbar to load before typing, as it is a good indicator of whether MathType has fully loaded.
- // This depends on the implementation of the modal and is not too desirable, but works well.
- .get(".wrs_toolbar")
- .should("be.visible");
-});
-
-Cypress.Commands.add("typeInTextEditor", (text) => {
- cy.getTextEditor().type(text);
-});
-
-Cypress.Commands.add("typeInModal", (formula) => {
- cy.get(".wrs_focusElementContainer > input").type(formula);
-});
-
-Cypress.Commands.add("clickModalButton", (button) => {
- switch (button) {
- case "insert":
- cy.get(".wrs_modal_button_accept").click();
- break;
- case "cancel":
- cy.get(".wrs_modal_button_cancel").click();
- break;
- case "confirmationClose":
- cy.get("#wrs_popup_accept_button").click({ force: true });
- break;
- case "confirmationCancel":
- cy.get("#wrs_popup_cancel_button").click({ force: true });
- break;
- case "xClose":
- cy.get(".wrs_modal_close_button").click();
- break;
- case "maximize":
- cy.get(".wrs_modal_maximize_button").click();
- break;
- case "stack":
- cy.get(".wrs_modal_stack_button").click();
- break;
- case "minimize":
- cy.get(".wrs_modal_minimize_button").click();
- break;
- case "hand":
- cy.get(".wrs_handWrapper > input").click();
- break;
- default:
- throw new Error(`The button '${button}' does not exist. Check the clickModalButton documentation.`);
- }
-});
-
-// eslint-disable-next-line no-unused-vars
-Cypress.Commands.add("insertFormulaFromScratch", (formula, chem = false, paste = false) => {
- // Open the mathtype modal
- cy.clickButtonToOpenModal(chem);
-
- // Type the formula that matxes the previous inserted text on the mathtype modal
- cy.typeInModal(formula);
-
- // Insert the formula
- cy.clickModalButton("insert");
-});
-
-Cypress.Commands.add("getFormula", (formulaId) => {
- cy.get(".Wirisformula")
- .should("have.length.at.least", formulaId + 1)
- .eq(formulaId);
-});
-
-Cypress.Commands.add("selectLatexFormula", (formulaId) => {
- let block;
- let startOffset;
- let endOffset;
-
- // Get the block, and offsets of the latex formula
- let countFormula = 0;
- const edit = Cypress.$("#editable");
- const kids = edit[0].children;
- for (let j = 0; j < kids.length; ++j) {
- const elem = kids[j];
- const html = elem.innerHTML;
- let prevDolar = false;
- let waitEndDolar = false;
- for (let i = 0; i < html.length; i++) {
- const caracter = html[i];
- if (caracter === "$" && prevDolar === false && waitEndDolar === false) {
- prevDolar = true;
- } else if (caracter === "$" && prevDolar === true && waitEndDolar === false) {
- prevDolar = false;
- waitEndDolar = true;
- if (countFormula === formulaId) startOffset = i + 1;
- } else if (caracter === "$" && prevDolar === false && waitEndDolar === true) {
- prevDolar = true;
- if (countFormula === formulaId) endOffset = i;
- else countFormula += 1;
- } else if (caracter === "$" && prevDolar === true && waitEndDolar === true) {
- prevDolar = false;
- waitEndDolar = false;
- }
- if (startOffset && endOffset) {
- block = j;
- break;
- }
- }
- if (startOffset && endOffset) break;
- }
-
- // Throw error if the latex formula identifier does not correspond to a latex formula on the test
- if (!startOffset || !endOffset) {
- throw new Error(`The latex formula number '${formulaId}' does not exist`);
- }
-
- // Select the latex formula
- cy.getTextEditor()
- .children()
- .eq(block)
- .trigger("mousedown")
- .then(($el) => {
- createSelection($el, startOffset, endOffset);
- })
- .trigger("mouseup");
- cy.document().trigger("selectionchange");
-});
-
-Cypress.Commands.add(
- "editFormula",
- { prevSubject: "optional" },
- (
- subject,
- formula,
- options = {
- chem: false,
- latex: false,
- formulaId: 0,
- },
- ) => {
- // Select the latex formula and edit it
- if (options.latex) {
- cy.selectLatexFormula(options.formulaId);
- cy.clickButtonToOpenModal(options.chem);
- cy.typeInModal(formula);
- cy.clickModalButton("insert");
- } else {
- throw new Error("Not implemented yet");
- }
- },
-);
-
-Cypress.Commands.add("pressESCButton", () => {
- cy.get("body").type("{esc}");
-});
diff --git a/cypress/support/index.js b/cypress/support/index.js
deleted file mode 100644
index fececccb1..000000000
--- a/cypress/support/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// ***********************************************************
-// This example support/index.js is processed and
-// loaded automatically before your test files.
-//
-// This is a great place to put global configuration and
-// behavior that modifies Cypress.
-//
-// You can change the location of this file or turn off
-// automatically serving support files with the
-// 'supportFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/configuration
-// ***********************************************************
-
-// Import commands.js using ES2015 syntax:
-import "./commands";
-import "./validations";
-import "cypress-fail-fast";
-
-// Alternatively you can use CommonJS syntax:
-// require('./commands')
diff --git a/cypress/support/utils.js b/cypress/support/utils.js
deleted file mode 100644
index a2d60c7c6..000000000
--- a/cypress/support/utils.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Create a selection in the document in the given field, at the given start and end positions.
- * @param field DOM element to make the selection in
- * @param start index of the character to start the selecion in
- * @param end index of the character to end the selection in
- */
-function createSelection(field, start, end) {
- const el = field[0];
- const document = el.ownerDocument;
- const range = document.createRange();
- range.selectNodeContents(el);
- document.getSelection().removeAllRanges(range);
- if (start) range.setStart(el.firstChild, start);
- if (end) range.setEnd(el.firstChild, end);
- document.getSelection().addRange(range);
-}
-
-// eslint-disable-next-line import/prefer-default-export
-export { createSelection };
diff --git a/cypress/support/validations.d.ts b/cypress/support/validations.d.ts
deleted file mode 100644
index 83fc62762..000000000
--- a/cypress/support/validations.d.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-///
-
-declare namespace Cypress {
- interface Chainable {
- /**
- * Validate that the height of the given formula and the surrounding text is the same.
- * Must be applied to a father command.
- * @param subject the formula to apply this validation to
- * @param preview {default = false} whether to check aligment on the preview or on the text editor
- */
- isAligned(subject: Element, preview?: boolean): Chainable;
-
- /**
- * Check that the given formula is rendered in the preview mode or the text editor area.
- * Must be applied to a father command.
- * @param subject the formula to apply this validation to
- * @param preview {default = false} whether to check aligment on the preview or on the text editor
- */
- isRendered(subject: Element, preview?: boolean): Chainable;
-
- /**
- * Validates that Hand mode is activated.
- */
- isHandModeOn();
-
- /**
- * Validates that the ChemType modal is open.
- */
- isChemTypeOn();
-
- /**
- * Check that the text inside the modal matches the given text string.
- * @param text the text to match the modal content against
- */
- modalTextEquals(text: string);
- }
-}
diff --git a/cypress/support/validations.js b/cypress/support/validations.js
deleted file mode 100644
index dc5f2cebd..000000000
--- a/cypress/support/validations.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// eslint-disable-next-line no-unused-vars
-Cypress.Commands.add("isRendered", { prevSubject: "element" }, (subject, preview = false) => {
- cy.wrap(subject).should("be.visible");
-});
diff --git a/cypress/tests/e2e/STD-018.insertion.js b/cypress/tests/e2e/STD-018.insertion.js
deleted file mode 100644
index cebb8a13a..000000000
--- a/cypress/tests/e2e/STD-018.insertion.js
+++ /dev/null
@@ -1,29 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-018
-// Title: User writes a latex formula and visualizes it on preview.
-// Document: https://docs.google.com/document/d/1fiGsUwqNIsjiaJI0aGfH_aNX5OJKEHkfWtfvlQkEEFI/edit
-// Context: E2E / Insertion
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("an inserted latex formula should be rendered on preview", function () {
- // Type the formula that matxes the previous inserted text on the mathtype modal
- cy.typeInTextEditor(this.formulas["latex-general"]);
-
- // // Click the update button
- // cy.get('#btn_update').click();
-
- // // Assert that the vertical align is -4px, which means that is aligner vertically (base) to the previous writen 2222
- // cy.getFormula(0).isRendered().and('have.attr', 'alt', this.formulas['latex-general-alt-en']);
-});
diff --git a/cypress/tests/e2e/STD-026.modal.js b/cypress/tests/e2e/STD-026.modal.js
deleted file mode 100644
index 94a61f222..000000000
--- a/cypress/tests/e2e/STD-026.modal.js
+++ /dev/null
@@ -1,38 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-026
-// Title: Validate Hand formulas open Hand directly when edited
-// Document: https://docs.google.com/document/d/10nBVV0y3O5Eo7hEHtok8-s8zsZqVId7s_jwT5vziy7g/edit
-// Context: E2E / Modal
-// Issue: -
-// ***********************************************************
-beforeEach(() => {
- // Load fixtures
- cy.fixture("formulas.json").as("formulas");
-
- // Visit page
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("Validate Hand formulas open Hand directly when edited", function () {
- // Click the MT button on the HTML editor toolbar
- cy.clickButtonToOpenModal();
-
- // Switch to Hand editing mode by clicking the Hand icon in the MT modal window
- // Draw a formula
- // Instead of drawing the formula by hand, we type them in and let Hand transform them
- cy.typeInModal(this.formulas["formula-general"]);
- cy.clickModalButton("hand");
-
- // Click the OK button in the MT modal window
- cy.clickModalButton("insert");
-
- // Double-click the created Hand formula
- cy.getFormula(0).dblclick();
-
- // MT modal window opens and Hand editing mode is already displayed with the formula
- cy.get("canvas").should("be.visible");
-});
diff --git a/cypress/tests/e2e/STD-028.modal.js b/cypress/tests/e2e/STD-028.modal.js
deleted file mode 100644
index 7bf395bf2..000000000
--- a/cypress/tests/e2e/STD-028.modal.js
+++ /dev/null
@@ -1,32 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-028
-// Title: Validate switch between CT and MT, and viceversa
-// Document: https://docs.google.com/document/d/1PlqZUsfta5GMXXRq89oy50NTW5zBQCeUi8YBdO83Yug/edit
-// Context: E2E / Modal
-// Issue: -
-// ***********************************************************
-beforeEach(() => {
- // Load fixtures
- cy.fixture("formulas.json").as("formulas");
-
- // Visit page
- cy.visit("/");
-});
-
-it("Validate switch between CT and MT, and viceversa", () => {
- // Click the MT icon in the HTML editor toolbar.
- cy.clickButtonToOpenModal();
-
- // Click the CT icon in the HTML editor toolbar
- cy.clickButtonToOpenModal(true);
-
- // MT modal window changes to CT modal window.
- cy.get(".wrs_modal_title").eq(0).should("have.text", "ChemType");
-
- // Click the MT icon in the HTML editor toolbar.
- cy.clickButtonToOpenModal();
-
- // CT modal window changes to MT modal window.
- cy.get(".wrs_modal_title").eq(0).should("have.text", "MathType");
-});
diff --git a/cypress/tests/sandbox/.gitkeep b/cypress/tests/sandbox/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/cypress/tests/smoke/.gitkeep b/cypress/tests/smoke/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/cypress/tests/smoke/STD-001.insertion.js b/cypress/tests/smoke/STD-001.insertion.js
deleted file mode 100644
index f8ef0540b..000000000
--- a/cypress/tests/smoke/STD-001.insertion.js
+++ /dev/null
@@ -1,29 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-001
-// Title: Validate alignment of a formula after insertion.
-// Document: https://docs.google.com/document/d/1RTZlelOssfwWAqx-ilTvRatrEQoaFIpk6ErWa7xMwIw/edit
-// Context: UI / Insertion
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("an inserted formula that looks like plain text should be aligned with the same plain text", function () {
- // Type the text plane on the text editor
- cy.typeInTextEditor(this.formulas["text-alignment"]);
-
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-alignment"]);
-
- // Assert that the vertical align is -4px, which means that is aligner vertically (base) to the previous writen 2222
- cy.getFormula(0).should("have.attr", "style").and("contain", "vertical-align: -4px");
-});
diff --git a/cypress/tests/smoke/STD-002.insertion.js b/cypress/tests/smoke/STD-002.insertion.js
deleted file mode 100644
index 42b4f6590..000000000
--- a/cypress/tests/smoke/STD-002.insertion.js
+++ /dev/null
@@ -1,32 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-002
-// Title: Validate alignment of a formula on preview.
-// Document: https://docs.google.com/document/d/1aAPzvAe8WEEXgZECLsmml07TG4l3Fdy3AlJiaFSp6Iw/edit
-// Context: UI / Insertion
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("an inserted formula that looks like plain text should be aligned with the same plane text on preview", function () {
- // Type the text plane on the text editor
- cy.typeInTextEditor(this.formulas["text-alignment"]);
-
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-alignment"]);
-
- // // Click the update button
- // cy.get('#btn_update').click();
-
- // // Assert that the vertical align is -4px, which means that is aligner vertically (base) to the previous writen 2222
- // cy.getFormula(1).should('have.attr', 'style').and('contain', 'vertical-align: -4px');
-});
diff --git a/cypress/tests/smoke/STD-003.insertion.js b/cypress/tests/smoke/STD-003.insertion.js
deleted file mode 100644
index d6818bc2a..000000000
--- a/cypress/tests/smoke/STD-003.insertion.js
+++ /dev/null
@@ -1,36 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-003
-// Title: Validate caret is placed after the inserted formula
-// Document: https://docs.google.com/document/d/1YjSGL5yfvdMQOrFrqL48tQ2vUgfxD6YNSUgqsH65xI8/edit
-// Context: UI / Insertion
-// Issue: -
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("Validate Hand formulas open Hand directly when edited", function () {
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-general"]);
-
- // User types the string ‘wiris’ on the HTML editor
- cy.typeInTextEditor("wiris");
-
- // The string wiris is written right after the formula
- cy.getTextEditor()
- .children()
- .first() // First paragraph
- .then(($p) => {
- // Get the second node inside the paragraph
- cy.wrap($p[0].childNodes[1].textContent);
- })
- .should("eq", "wiris");
-});
diff --git a/cypress/tests/smoke/STD-004.images.js b/cypress/tests/smoke/STD-004.images.js
deleted file mode 100644
index 648bdb82e..000000000
--- a/cypress/tests/smoke/STD-004.images.js
+++ /dev/null
@@ -1,28 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-004
-// Title: Validate wiris formulas contain class with value Wirisformula.
-// Document: https://docs.google.com/document/d/1LCM0z-kmZKdwpSMnrosMsyRVmZ5Zg5TNw_-VEkYyZng/edit
-// Context: Integration / Image
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("formula should have wirisformula class", function () {
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-general"]);
-
- // Get the formula by it's alt text and assert it has the Wirisformula class
- // We could find the formula by using getFormula, but internally, that looks for
- // .Wirisformula, so it defeats the purpose. That's why we use the alt instead.
- cy.get(`img[alt="${this.formulas["formula-general-alt-es"]}"]`).should("have.class", "Wirisformula");
-});
diff --git a/cypress/tests/smoke/STD-005.images.js b/cypress/tests/smoke/STD-005.images.js
deleted file mode 100644
index d0673d1d4..000000000
--- a/cypress/tests/smoke/STD-005.images.js
+++ /dev/null
@@ -1,35 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-005
-// Title: Validate formula height and width is correct.
-// Document: https://docs.google.com/document/d/167zTPA2JxtbPaxdCp8kBKIEHoRyjHZnIWYmOPLEEMY4/edit
-// Context: UI / Image
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("The inserted formula should have the correct width and height", () => {
- // Open the mathtype modal
- cy.clickButtonToOpenModal();
-
- // Write a mathtype formula: x/3
- cy.typeInModal("{ctrl}/").type("x").type("{downarrow}3");
-
- // Insert the written formula by clicking the insert button on the modal
- cy.clickModalButton("insert");
-
- // Get the previous inserted formula
- cy.getFormula(0).then(($formula) => {
- const formula = $formula[0];
-
- // Assert that the width and height are the ones writen in the test case for the inserted formula
- expect(formula.width).to.equal(18);
- expect(formula.height).to.equal(41);
- });
-});
diff --git a/cypress/tests/smoke/STD-007.images.js b/cypress/tests/smoke/STD-007.images.js
deleted file mode 100644
index d743c7340..000000000
--- a/cypress/tests/smoke/STD-007.images.js
+++ /dev/null
@@ -1,26 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-007
-// Title: Validate wiris formulas contain alt attribute.
-// Document: https://docs.google.com/document/d/1Sa83zG7-sRpS1WIPQNTLtaeUFrwqZVSpdTbrtnkZHVI/edit
-// Context: UI / Image
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("A wiris formula should have the alt attribute", function () {
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-general"]);
-
- // Get the previous inserted formula
- cy.getFormula(0).should("have.attr", "alt");
-});
diff --git a/cypress/tests/smoke/STD-010.images.js b/cypress/tests/smoke/STD-010.images.js
deleted file mode 100644
index 12ee07397..000000000
--- a/cypress/tests/smoke/STD-010.images.js
+++ /dev/null
@@ -1,30 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-010
-// Title: Validate the formula is the same on editing mode and on preview .
-// Document: https://docs.google.com/document/d/1bRxBBG_OLS_1HTGdOBRbHHIRJPZHboVJ2Zy68z80fyY/edit
-// Context: UI / Images
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("an inserted formula should be the same on preview when this is updated", function () {
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-general"], true);
-
- // // Click the update button
- // cy.get('#btn_update').click();
-
- // // Assert that the vertical align is -4px, which means that is aligner vertically (base) to the previous writen 2222
- // cy.getFormula(0).isRendered().and('have.attr', 'alt', this.formulas['formula-general-alt-es']);
- // cy.getFormula(1).isRendered().and('have.attr', 'alt', this.formulas['formula-general-alt-en']);
-});
diff --git a/cypress/tests/smoke/STD-011.modal.js b/cypress/tests/smoke/STD-011.modal.js
deleted file mode 100644
index 0f11b4449..000000000
--- a/cypress/tests/smoke/STD-011.modal.js
+++ /dev/null
@@ -1,20 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-011
-// Title: Check Hand icon is visible.
-// Document: https://docs.google.com/document/d/12cxOZRwLVhE_Aby2Ckjjee2WWJcTuceOcXmuBgZBAlE/edit
-// Context: UI / Modal
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Visit the page.
- cy.visit("/");
-});
-
-it("Hand icon should be visible on the mathtype modal", () => {
- // Open the mathtype modal
- cy.clickButtonToOpenModal();
-
- // Check that the hand button is visible on mathtype modal
- cy.get(".wrs_handWrapper > input").should("be.visible");
-});
diff --git a/cypress/tests/smoke/STD-012.modal.js b/cypress/tests/smoke/STD-012.modal.js
deleted file mode 100644
index e8c73abc2..000000000
--- a/cypress/tests/smoke/STD-012.modal.js
+++ /dev/null
@@ -1,32 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-011
-// Title: Check Minimize, Maximize, and Close icons are visible in the modal.
-// Document: https://docs.google.com/document/d/1soW156YvORb3TIumKmlIjhpRccme5c_YyQhVLypIh0s/edit
-// Context: UI / Modal
-// Issue: KB-13069
-// ***********************************************************
-describe("Resize modal icons are visible", () => {
- beforeEach(() => {
- // Visit the page.
- cy.visit("/");
-
- // Open the mathtype modal
- cy.clickButtonToOpenModal();
- });
-
- it("minimize icon should be visible on mathtype modal", () => {
- // Check that minimize button is visible on mathtype modal
- cy.get(".wrs_modal_minimize_button").should("be.visible");
- });
-
- it("maximize icon should be visible on mathtype modal", () => {
- // Check that minimize button is visible on mathtype modal
- cy.get(".wrs_modal_maximize_button").should("be.visible");
- });
-
- it("close icon should be visible on mathtype modal", () => {
- // Check that minimize button is visible on mathtype modal
- cy.get(".wrs_modal_close_button").should("be.visible");
- });
-});
diff --git a/cypress/tests/smoke/STD-014.insertion.js b/cypress/tests/smoke/STD-014.insertion.js
deleted file mode 100644
index 752ff3d32..000000000
--- a/cypress/tests/smoke/STD-014.insertion.js
+++ /dev/null
@@ -1,23 +0,0 @@
-///
-
-beforeEach(() => {
- // Load fixtures
- cy.fixture("formulas.json").as("formulas");
-
- // Visit page
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("User creates a new formula from scratch using MT", function () {
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-general"]);
-
- // MT editor modal window is closed.
- cy.get(".wrs_modal_dialogContainer").should("not.to.be.visible");
-
- // The formula is inserted at the beginning of the HTML editor content and perfectly rendered
- cy.getFormula(0).isRendered();
-});
diff --git a/cypress/tests/smoke/STD-016.insertion.js b/cypress/tests/smoke/STD-016.insertion.js
deleted file mode 100644
index 807c9d38d..000000000
--- a/cypress/tests/smoke/STD-016.insertion.js
+++ /dev/null
@@ -1,46 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-016
-// Title: User edits a formula by Double-click and inserts it.
-// Document: https://docs.google.com/document/d/1bIZOmDigkvhMCpAcTf81nz3Wp252aZpyPol9AxY0OXY/edit
-// Context: E2E / Insertion
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("should be able to edit an existing formula", { retries: 3 }, function () {
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-general"]);
-
- // Double-click the previous inserted formula to start editing it
- cy.getFormula(0).dblclick();
-
- // Assert that the toolbar is visible so that we know the modal is fully loaded
- cy.get(".wrs_toolbar").should("be.visible");
-
- // Wait for the formula clocked to be loaded
- cy.get(".wrs_container").invoke("text").should("contain", "y"); // .children().should('have.length.at.least', 9);
-
- // Modify the opened formula
- cy.get(".wrs_focusElement").click().type(this.formulas["formula-addition"]);
- // cy.typeInModal('{movetostart}{del}{del}{del}');
-
- // Click the insert button on the mathtype modal to insert the previous edited formula
- cy.clickModalButton("insert");
-
- // Expect the formula to be edited propertly
- cy.getFormula(0)
- .should("have.attr", "alt")
- .then((alt) => {
- expect(alt).to.equal(this.formulas["formula-total-alt-es"]);
- });
-});
diff --git a/cypress/tests/smoke/STD-017.insertion.js b/cypress/tests/smoke/STD-017.insertion.js
deleted file mode 100644
index 769c3e2e3..000000000
--- a/cypress/tests/smoke/STD-017.insertion.js
+++ /dev/null
@@ -1,46 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-017
-// Title: User edits a formula and cancels the edition.
-// Document: https://docs.google.com/document/d/1CqlEq9p0oVrRhpXjaguehtA4LBl9157qSZ_vg0pdswM/edit
-// Context: E2E / Insertion
-// Issue: KB-13069
-// ***********************************************************
-
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("should be able to edit and existing formula and cancel the edition", function () {
- // Insert a new MathType formula from scratch on the editor
- cy.insertFormulaFromScratch(this.formulas["formula-general"]);
-
- // Double-click the previous inserted formula to start editing it
- cy.getFormula(0).dblclick();
-
- // Edit the opened formula by adding some other content (=y)
- cy.typeInModal(this.formulas["formula-addition"]);
-
- // Click the cancel button after editing the formula on the mathtype modal
- cy.clickModalButton("cancel");
-
- // CLick the close button on the confirmation close mathtype modal the cancel all changes and close it
- cy.clickModalButton("confirmationClose");
-
- // Assert the formula has no changes
- cy.getFormula(0)
- .should("have.attr", "alt")
- .then((alt) => {
- expect(alt).to.equal(this.formulas["formula-general-alt-es"]);
- });
-
- // Verify the formula is propertly rendered
- cy.getFormula(0).isRendered();
-});
diff --git a/cypress/tests/smoke/STD-019.insertion.js b/cypress/tests/smoke/STD-019.insertion.js
deleted file mode 100644
index 9b43252c8..000000000
--- a/cypress/tests/smoke/STD-019.insertion.js
+++ /dev/null
@@ -1,33 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-019
-// Title: User edits a latex formula.
-// Document: https://docs.google.com/document/d/1tkYS_g5ZZcjIiUT-nMPv4G2AGlcumw9siTY_B1bFtkE/edit
-// Context: E2E / Insertion
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Load fixture data
- cy.fixture("formulas.json").as("formulas");
-
- // Visit the page.
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("should be able to edit an existing latex formula", function () {
- // Write a latex formula on the text editor
- cy.typeInTextEditor(this.formulas["latex-general"]);
-
- // Edit the first latex formula with mathtype
- cy.editFormula(this.formulas["latex-addition"], {
- chem: false,
- latex: true,
- formulaId: 0,
- });
-
- // Expect that the text editor contains the latex formula
- cy.getTextEditor().invoke("text").should("contain", "$$\\cos^2(x)+\\sin^2(x)=\\log(e)$$");
-});
diff --git a/cypress/tests/smoke/STD-020.modal.js b/cypress/tests/smoke/STD-020.modal.js
deleted file mode 100644
index 14b85859c..000000000
--- a/cypress/tests/smoke/STD-020.modal.js
+++ /dev/null
@@ -1,23 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-020
-// Title: User opens MT and closes it via ESC
-// Document: https://docs.google.com/document/d/1v6NyWfvoFrgX7CWufN12_tCzyO9ITvX3HTAXTIYWCSQ/edit
-// Context: E2E / Modal
-// Issue: -
-// ***********************************************************
-beforeEach(() => {
- // Visit page
- cy.visit("/");
-});
-
-it("User opens MT and closes it via ESC", () => {
- // Click the MT button in the HTML editor toolbar
- cy.clickButtonToOpenModal();
-
- // Press the ESC key
- cy.pressESCButton();
-
- // MT editor modal window is closed
- cy.get(".wrs_modal_dialogContainer").should("not.be.visible");
-});
diff --git a/cypress/tests/smoke/STD-021.modal.js b/cypress/tests/smoke/STD-021.modal.js
deleted file mode 100644
index 02076ec3c..000000000
--- a/cypress/tests/smoke/STD-021.modal.js
+++ /dev/null
@@ -1,23 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-021
-// Title: Open and close the mathtype modal without adding any formula.
-// Document: https://docs.google.com/document/d/1MloNEApADlavZHfODqScyGCNUrZyMgiGYrPx46c4waQ/edit
-// Context: E2E / Modal
-// Issue: KB-13069
-// ***********************************************************
-beforeEach(() => {
- // Visit the page.
- cy.visit("/");
-});
-
-it("should be able to edit and existing formula and cancel the edition", () => {
- // Open the mathtype modal bu clicking the mathtype button
- cy.clickButtonToOpenModal();
-
- // Click the cancel button on the mathtype modal to close the modal
- cy.clickModalButton("cancel");
-
- // Verify the modal is closed
- cy.get(".wrs_focusElement").should("not.be.visible");
-});
diff --git a/cypress/tests/smoke/STD-022.modal.js b/cypress/tests/smoke/STD-022.modal.js
deleted file mode 100644
index 77a8e842e..000000000
--- a/cypress/tests/smoke/STD-022.modal.js
+++ /dev/null
@@ -1,40 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-022
-// Title: User opens MT, edits an equation and Cancels. Accepts the ‘are you sure you want to leave?’ dialog
-// Document: https://docs.google.com/document/d/11R4j3ZW0a50Lp02frqihtfZPeBFAYfN_xtqrs3AejdM/edit
-// Context: E2E / Modal
-// Issue: -
-// ***********************************************************
-beforeEach(() => {
- // Load fixtures
- cy.fixture("formulas.json").as("formulas");
-
- // Visit page
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("User opens MT, edits an equation and Cancels. Accepts the ‘are you sure you want to leave?’ dialog", function () {
- // Click on the MT icon in the HTML editor toolbar
- cy.clickButtonToOpenModal();
-
- // Type the formula
- cy.typeInModal(this.formulas["formula-general"]);
-
- // Click the Cancel button in the MT editor
- cy.clickModalButton("cancel");
-
- // Click the Close button from the ‘Are you sure you want to leave?’ dialog
- cy.clickModalButton("confirmationClose");
-
- // The MT editor modal window is closed and...
- cy.get(".wrs_modal_dialogContainer").should("not.to.be.visible");
-
- // ... no formula is inserted to the HTML editor
- // We check for the 2nd formula, as currently the demos come with one formula by default
- // (So one .Wirisformula in the editor and one .Wirisformula in the preview)
- cy.get(".Wirisformula").eq(1).should("not.exist");
-});
diff --git a/cypress/tests/smoke/STD-023.modal.js b/cypress/tests/smoke/STD-023.modal.js
deleted file mode 100644
index 583add900..000000000
--- a/cypress/tests/smoke/STD-023.modal.js
+++ /dev/null
@@ -1,40 +0,0 @@
-///
-// ***********************************************************
-// Test case: INT-STD-023
-// Title: User opens MT, edits an equation and closes the modal via X button. Denies the ‘are you sure you want to leave?’ dialog and inserts the formula
-// Document: https://docs.google.com/document/d/1EaC9zB9eIADTk06j3TyPouOwzIp3AHael5zV39kOMyM/edit
-// Context: E2E / Modal
-// Issue: -
-// ***********************************************************
-beforeEach(() => {
- // Load fixtures
- cy.fixture("formulas.json").as("formulas");
-
- // Visit page
- cy.visit("/");
-
- // Clear the editor content in order to reduce noise
- cy.getTextEditor().clear();
-});
-
-it("User opens MT, edits an equation and closes the modal via X button. Denies the ‘are you sure you want to leave?’ dialog and inserts the formula", function () {
- // Click on the MT icon in the HTML editor toolbar
- cy.clickButtonToOpenModal();
-
- // Type the formula
- cy.typeInModal(this.formulas["formula-general"]);
-
- // Click the ‘X’ button of the MT modal window in order to close it
- cy.clickModalButton("xClose");
-
- // Click the Cancel button from the ‘Are you sure you want to leave?’ dialog
- cy.clickModalButton("confirmationCancel");
-
- // Click the Insert button in the MT modal window
- cy.clickModalButton("insert");
-
- // The formula is rendered in the HTML editor
- // We check for the 3rd formula, as currently the demos come with one formula by default
- // (So one .Wirisformula in the editor and one .Wirisformula in the preview)
- cy.getFormula(0).should("be.visible");
-});
diff --git a/cypress/tests/ui/.gitkeep b/cypress/tests/ui/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/cypress/tests/validation/.gitkeep b/cypress/tests/validation/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/demos/html/ckeditor4/project.json b/demos/html/ckeditor4/project.json
index 7810740a5..380718602 100644
--- a/demos/html/ckeditor4/project.json
+++ b/demos/html/ckeditor4/project.json
@@ -51,6 +51,16 @@
"lintFilePatterns": ["demos/html/ckeditor4/**/*.{ts,tsx,js,jsx}"]
},
"outputs": ["{options.outputFile}"]
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "staticFilePath": "demos/html/ckeditor4/",
+ "port": 8001,
+ "spa": false,
+ "watch": false
+ }
}
}
}
diff --git a/demos/html/ckeditor5/project.json b/demos/html/ckeditor5/project.json
index 689e722fa..5b41a0d58 100644
--- a/demos/html/ckeditor5/project.json
+++ b/demos/html/ckeditor5/project.json
@@ -51,6 +51,16 @@
"lintFilePatterns": ["demos/html/ckeditor5/**/*.{ts,tsx,js,jsx}"]
},
"outputs": ["{options.outputFile}"]
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "staticFilePath": "demos/html/ckeditor5/",
+ "port": 8002,
+ "spa": false,
+ "watch": false
+ }
}
}
}
diff --git a/demos/html/froala/project.json b/demos/html/froala/project.json
index 8aab2e250..4694ea35e 100644
--- a/demos/html/froala/project.json
+++ b/demos/html/froala/project.json
@@ -51,6 +51,16 @@
"lintFilePatterns": ["demos/html/froala/**/*.{ts,tsx,js,jsx}"]
},
"outputs": ["{options.outputFile}"]
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "staticFilePath": "demos/html/froala/",
+ "port": 8004,
+ "spa": false,
+ "watch": false
+ }
}
}
}
diff --git a/demos/html/generic/project.json b/demos/html/generic/project.json
index 0d4aabc02..0a24290a8 100644
--- a/demos/html/generic/project.json
+++ b/demos/html/generic/project.json
@@ -51,6 +51,16 @@
"lintFilePatterns": ["demos/html/generic/**/*.{ts,tsx,js,jsx}"]
},
"outputs": ["{options.outputFile}"]
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "staticFilePath": "demos/html/generic/",
+ "port": 8007,
+ "spa": false,
+ "watch": false
+ }
}
}
}
diff --git a/demos/html/tinymce5/project.json b/demos/html/tinymce5/project.json
index e52f261d8..c55e604eb 100644
--- a/demos/html/tinymce5/project.json
+++ b/demos/html/tinymce5/project.json
@@ -51,6 +51,16 @@
"lintFilePatterns": ["demos/html/tinymce5/**/*.{ts,tsx,js,jsx}"]
},
"outputs": ["{options.outputFile}"]
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "staticFilePath": "demos/html/tinymce5/",
+ "port": 8006,
+ "spa": false,
+ "watch": false
+ }
}
}
}
diff --git a/demos/html/tinymce6/project.json b/demos/html/tinymce6/project.json
index b64f662b1..0bbc86e0e 100644
--- a/demos/html/tinymce6/project.json
+++ b/demos/html/tinymce6/project.json
@@ -51,6 +51,16 @@
"lintFilePatterns": ["demos/html/tinymce6/**/*.{ts,tsx,js,jsx}"]
},
"outputs": ["{options.outputFile}"]
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "staticFilePath": "demos/html/tinymce6/",
+ "port": 8008,
+ "spa": false,
+ "watch": false
+ }
}
}
}
diff --git a/demos/html/tinymce7/project.json b/demos/html/tinymce7/project.json
index b6f405d2b..516a47b74 100644
--- a/demos/html/tinymce7/project.json
+++ b/demos/html/tinymce7/project.json
@@ -51,6 +51,16 @@
"lintFilePatterns": ["demos/html/tinymce7/**/*.{ts,tsx,js,jsx}"]
},
"outputs": ["{options.outputFile}"]
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "staticFilePath": "demos/html/tinymce7/",
+ "port": 8009,
+ "spa": false,
+ "watch": false
+ }
}
}
}
diff --git a/demos/html/tinymce8/project.json b/demos/html/tinymce8/project.json
index 5667ad149..a7da5967a 100644
--- a/demos/html/tinymce8/project.json
+++ b/demos/html/tinymce8/project.json
@@ -51,6 +51,16 @@
"lintFilePatterns": ["demos/html/tinymce8/**/*.{ts,tsx,js,jsx}"]
},
"outputs": ["{options.outputFile}"]
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build"],
+ "options": {
+ "staticFilePath": "demos/html/tinymce8/",
+ "port": 8010,
+ "spa": false,
+ "watch": false
+ }
}
}
}
diff --git a/docs/development/cicd/README.md b/docs/development/cicd/README.md
index 9cdb40d03..5b6296287 100644
--- a/docs/development/cicd/README.md
+++ b/docs/development/cicd/README.md
@@ -15,28 +15,27 @@ This project uses [GitHub actions](https://github.com/features/actions) for the
This job uses JSDoc library to generate a static site as an artifact called `mathtype-html-integration-devkit-docs.zip`, from the comments on the library code.
-### Run Cypress tests with npm packages
+### Run E2E tests
+This workflow runs end-to-end tests across all supported HTML editors using Playwright. The tests are executed in a matrix strategy to enable parallel execution for each editor. See [docs/testing/README.md](../testing/README.md) for further details.
-**[Deprecated]**
+**Supported editors:**
+- Generic HTML
+- CKEditor 4 & 5
+- Froala
+- TinyMCE 5, 6, 7 & 8
-Builds the packages using the source code available at npmjs and runs all available Cypress tests.
+**Key features:**
+- **Parallel execution**: Each editor runs in its own job for faster feedback
+- **Multi-browser testing**: Tests run on Chromium, Firefox, and WebKit
+- **Timeout protection**: Each job has a 30-minute timeout to prevent hanging
+- **Test reporting**: Results are published using JUnit format with detailed reports
+- **Artifact collection**: Test reports are collected as downloadable artifacts
-- **On schedule**: every Monday at 1AM. It sends the test data to [Cypress Dashboard][cypress-dashboard]. It can be run on any branch.
+**Workflow triggers:**
+- Push to master branch
+- Pull requests
+- Manual dispatch
-- **On demand**: a manual trigger that allows the user to send data to [Cypress Dashboard][cypress-dashboard], optionally.
+The workflow builds the necessary packages, starts static file servers for each editor demo, and runs the Playwright test suite against them.
-## Actions secrets
-Secrets are GitHub environment variables that are encrypted. Anyone with collaborator access to this repository can use these secrets for Actions.
-
-| Name | Description |
-| ------------------ | ----------------------------------------------------------------------------------------------------------- |
-| GH_CICD_TOKEN | A GitHub token to allow detecting a build vs a re-run build. [More][cypress-action] |
-| CYPRESS_PROJECT_ID | A 6 character string unique identifier for the project. |
-| CYPRESS_RECORD_KEY | Cypress record key is an authentication key that allows to send record tests data to the Dashboard Service. |
-
-[Visit Secrets page at GitHub][secrets].
-
-[secrets]: https://github.com/wiris/html-integrations/settings/secrets
-[cypress-dashboard]: (https://cypress.io/dashboard/)
-[cypress-action]: https://github.com/cypress-io/github-action
diff --git a/docs/development/testing/README.md b/docs/development/testing/README.md
index 5a129f87e..ef2af6066 100644
--- a/docs/development/testing/README.md
+++ b/docs/development/testing/README.md
@@ -1,52 +1,190 @@
-# Testing
+# E2E Testing Documentation
-[MathType Web Integrations](../../../README.md) → [Documentation](../../README.md) → [Development guide](../README.md) → Testing
+## Overview
-This project uses [Cypress][Cypress] to run integration and validation tests in order to cover all published packages.
+This project uses Playwright for end-to-end testing across multiple HTML editor integrations. The testing setup supports parallel execution across different editors with configurable environments.
-[Cypress]: https://www.cypress.io/
+## Test Structure
-## Table of contents
+The E2E tests are located in `/tests/e2e/` with the following structure:
-- [Run all tests at once](#run-all-tests-at-once)
-- [Run all the tests for a specific demo](#run-all-the-tests-for-a-specific-demo)
+```
+tests/e2e/
+├── .env.example # Environment configuration template
+├── .env # (Optional) Local environment configuration; git-ignored
+├── playwright.config.ts # Playwright configuration
+├── enums/ # Shared enums for test logic
+├── helpers/ # Utility/helper functions
+├── interfaces/ # Shared TypeScript interfaces
+├── page-objects/ # Page object models
+│ └── base_editor.ts # Base editor class
+│ └── html/ # Page object for each editor test page
+└── tests/ # Test specifications
+ ├── edit/ # Formula editing tests
+ │ ├── edit_corner_cases.spec.ts
+ │ ├── edit_hand.spec.ts
+ │ ├── edit_via_doble_click.spec.ts
+ │ └── edit_via_selection.spec.ts
+ ├── editor/ # Editor functionality tests
+ │ ├── copy_cut_drop.spec.ts
+ │ └── editor.spec.ts
+ ├── insert/ # Formula insertion tests
+ │ ├── insert_corner_cases.spec.ts
+ │ ├── insert_hand.spec.ts
+ │ └── insert.spec.ts
+ ├── latex/ # LaTeX functionality tests
+ │ └── latex.spec.ts
+ ├── modal/ # Modal dialog tests
+ │ ├── confirmation_dialog.spec.ts
+ │ └── toolbar.spec.ts
+ └── telemetry/ # Analytics tests
+ └── telemetry.spec.ts
+```
-## Before you begin
+## Supported Editors and Packages
-Linux users will need to install `net-tools` to use Cypress.
+The testing framework supports the following HTML editors with their corresponding localhost ports:
+| Editor | Port | Status |
+|------------|------|--------|
+| ckeditor4 | 8001 | ✅ Active |
+| ckeditor5 | 8002 | ✅ Active |
+| froala | 8004 | ✅ Active |
+| tinymce5 | 8006 | ✅ Active |
+| tinymce6 | 8008 | ✅ Active |
+| tinymce7 | 8009 | ✅ Active |
+| tinymce8 | 8010 | ✅ Active |
+| generic | 8007 | ✅ Active |
+| viewer | ? | 📋 TODO |
-```bash
-$ sudo apt install net-tools
-```
-Also, you will need to allow non-local connections to control the X server on your computer.
+## Environment Configuration
+
+### Local Setup
+You can configure your environment using an optional `.env` file or by setting variables directly in the CLI command, as explained below.
+
+1. **Copy the environment template:**
+ ```bash
+ cp tests/e2e/.env.example tests/e2e/.env
+ ```
+
+2. **Configure your environment:**
+ ```bash
+ # tests/e2e/.env
+
+ # Select editors to test (pipe-separated)
+ HTML_EDITOR=generic|ckeditor4|ckeditor5
-Run this command:
+ # Environment selection
+ USE_STAGING=false
+
+ # Branch for staging tests
+ TEST_BRANCH=master
+
+ # API Keys for commercial editors. Required to deploy the test page.
+ CKEDITOR4_API_KEY=
+ FROALA_API_KEY=
+ ```
+
+### Environment Variables
+
+| Variable | Description | Default | Example | Required |
+|----------|-------------|---------|---------|----------|
+| `HTML_EDITOR` | Pipe-separated list of editors to test | All editors | `generic\|ckeditor5` | Yes |
+| `USE_STAGING` | Use staging environment vs localhost | `false` | `true\|false` | No |
+| `TEST_BRANCH` | Git branch for staging tests | `master` | `feature-branch` | No |
+| `CKEDITOR4_API_KEY` | API key for CKEditor 4 commercial features | None | `your-ckeditor4-key` | For all CKEditor 4 tests |
+| `FROALA_API_KEY` | API key for Froala Editor commercial features | None | `your-froala-key` | For Froala licensed features only |
+
+
+## Running Tests
+
+### Prerequisites
```bash
-$ xhost local:root
+# Install dependencies
+yarn install
+
+# Install Playwright browsers
+yarn playwright install --with-deps
```
-> This has to be executed once after each reboot
+### Local Development
+The `yarn test:e2e` script is defined in the main package.json and runs the E2E tests.
-## Run all tests at once
+Playwright is configured to pre-build and deploy both the package and test site (`demos` folder) for the configured
+editors and deploy them in order to run the test. Don't pre-deploy the test page, Playwright will do it by itself.
-Before running the tests you will need to build all package and start all demos.
+```bash
+# Run tests for specific editors
+HTML_EDITOR=ckeditor5 yarn test:e2e
-All tests can be run with the commands:
+# Run tests for multiple editors
+HTML_EDITOR=generic|froala yarn test:e2e
-```sh
-$ nx run-many --target=start --all --parallel
-$ nx run-many --target=test --all --parallel
-```
+# Run all tests for all editors. If no HTML_EDITOR variable is set, all editors are tested
+yarn test:e2e
-## Run all the tests for a specific demo
+# Run with staging environment
+USE_STAGING=true yarn test:e2e
-You can run all tests for a specific demo with the `nx test ` command.
+# Run specific browser
+yarn test:e2e --project=webkit
-Before running the tests you will need to build the package and start a demo. For example to run all tests on the `ckeditor5` demo run:
+# Run in headed mode
+yarn test:e2e --headed
+# Run specific test file
+yarn test:e2e tests/insert/insert.spec.ts
```
-$ nx start html-ckeditor5
-$ nx test ckeditor5
+[See the official Playwright CLI documentation](https://playwright.dev/docs/test-cli) for more details on available commands and options.
+
+
+**Example workflow:**
+```bash
+# Build and test CKEditor5
+yarn
+HTML_EDITOR=ckeditor5 yarn test:e2e
```
+
+## Playwright Configuration
+See ([`playwright.config.ts`](../../../tests/e2e/playwright.config.ts)).
+
+
+## CI/CD Integration
+
+### GitHub Actions Workflow
+
+The E2E tests are automated via GitHub Actions ([`run-e2e-tests.yml`](../../../.github/workflows/cypress-Run-tests-with-npm-packages.yml)):
+
+- **When tests run**: On pushes to `main`, pull requests, and manual workflow dispatch
+- **Parallelization**: Each editor runs in a separate job using matrix strategy for maximum parallel execution
+- **Reports**:
+ - Github reports appear in the GitHub Actions **Checks** tab
+ - Failed tests create GitHub Actions annotations with direct links for quick debugging.
+ - A single HTML report is generated and attached to the workflow run. Contains results for all Editors.
+
+## Test Coverage
+
+| Test File | Category | Description |
+|-----------|----------|-------------|
+| `edit/edit_corner_cases.spec.ts` | Edit | Edge cases and error conditions in formula editing |
+| `edit/edit_hand.spec.ts` | Edit | Manual formula modifications and handwriting input |
+| `edit/edit_via_doble_click.spec.ts` | Edit | Editing formulas by double-clicking on existing formulas |
+| `edit/edit_via_selection.spec.ts` | Edit | Editing formulas via text selection and context menu |
+| `editor/copy_cut_drop.spec.ts` | Editor | Clipboard operations (copy/cut/paste) and drag-drop functionality |
+| `editor/editor.spec.ts` | Editor | General editor behavior and integration tests |
+| `insert/insert_corner_cases.spec.ts` | Insert | Edge cases and error conditions in formula insertion |
+| `insert/insert_hand.spec.ts` | Insert | Manual formula creation via handwriting input |
+| `insert/insert.spec.ts` | Insert | Standard formula insertion workflows and toolbar interactions |
+| `latex/latex.spec.ts` | LaTeX | LaTeX rendering, parsing, and conversion functionality |
+| `modal/confirmation_dialog.spec.ts` | Modal | Confirmation dialog interactions and user workflows |
+| `modal/toolbar.spec.ts` | Modal | Toolbar modal functionality and behavior |
+| `telemetry/telemetry.spec.ts` | Telemetry | Usage metrics, event tracking, and analytics validation |
+
+
+# TODOs
+This project previously used cypress for E2E testing. There might still be some reference to Cypress in the code (e.g.: see test section in the demos `project.json` files). These must be deleteded.
+- Remove cypress refereces in the `project.json` files
+- Remove cypress dashboard secrets in the repository
+- Remove old documentation cypress references.
+
diff --git a/package.json b/package.json
index c2a3b8f2c..87eb9274f 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,7 @@
"preinstall": "node packages/res/git-data.mjs",
"postinstall": "rm -rf ~/.config/yarn/link/* && for d in packages/*/ ; do (cd $d && yarn link); done",
"test-old": "node scripts/services/executeTests.js",
- "test": "nx run-many --target=test --all --parallel",
- "test:ci": "docker run -v $PWD:/cypress --net=host -w /cypress -e CYPRESS_PROJECT_ID --entrypoint=cypress cypress/included:7.5.0 run --project .",
+ "test:e2e": "playwright test --config=tests/e2e/playwright.config.ts",
"build": "cd demos/html/generic && npm install && npm start &",
"lint": "prettier --write . --ignore-path .gitignore --ignore-path .prettierignore --ignore-path .eslintignore && nx run-many --target=lint --all --parallel --fix"
},
@@ -18,22 +17,23 @@
"@babel/eslint-parser": "^7.24.1",
"@nrwl/js": "18.2.2",
"@nrwl/tao": "18.2.2",
- "@nx/cypress": "18.2.2",
"@nx/eslint-plugin": "18.2.2",
"@nx/linter": "18.2.2",
"@nx/web": "18.2.2",
"@nx/webpack": "18.2.2",
"@nx/workspace": "18.2.2",
+ "@playwright/test": "^1.40.0",
"@types/node": "20.12.4",
"clean-webpack-plugin": "^4.0.0",
+ "dotenv": "^16.3.1",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
+ "eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-jsdoc": "^48.2.3",
+ "eslint-plugin-prettier": "^5.2.3",
"html-validate": "^8.18.1",
"nx": "18.2.2",
- "eslint-config-prettier": "^10.0.1",
- "eslint-plugin-prettier": "^5.2.3",
"prettier": "3.5.2",
"typescript": "~5.4.4"
},
@@ -43,5 +43,8 @@
"packages/**",
"demos/**"
],
- "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
+ "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610",
+ "dependencies": {
+ "serve": "^14.2.5"
+ }
}
diff --git a/tests/e2e/.env.example b/tests/e2e/.env.example
new file mode 100644
index 000000000..6fbf41fd5
--- /dev/null
+++ b/tests/e2e/.env.example
@@ -0,0 +1,16 @@
+# Environment configuration for HTML Editors E2E Tests
+# File must be named .env in the root of the project
+
+# HTML Editor selection - pipe separated list of editors to test
+# Available options: generic|ckeditor4|ckeditor5|froala|tinymce5|tinymce6|tinymce7|tinymce8
+HTML_EDITOR=generic|ckeditor4|ckeditor5|froala|tinymce5|tinymce6|tinymce7|tinymce8
+
+# Whether to use the staging environment or use localhost
+USE_STAGING=false
+
+# Branch to test. Only applies when USE_STAGING=true
+TEST_BRANCH=master
+
+# API Keys for commercial editors. Required to deploy the test page.
+CKEDITOR4_API_KEY=
+FROALA_API_KEY=
diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore
new file mode 100644
index 000000000..2ee681b85
--- /dev/null
+++ b/tests/e2e/.gitignore
@@ -0,0 +1,9 @@
+# Ignore node modules and environment files
+node_modules
+.env
+.tmp
+
+# Ignore Playwright test reports and results
+playwright-report
+test-results
+screenshots
diff --git a/tests/e2e/enums/equation_entry_mode.ts b/tests/e2e/enums/equation_entry_mode.ts
new file mode 100644
index 000000000..a719d6890
--- /dev/null
+++ b/tests/e2e/enums/equation_entry_mode.ts
@@ -0,0 +1,6 @@
+enum EquationEntryMode {
+ MATHML,
+ LATEX,
+}
+
+export default EquationEntryMode
\ No newline at end of file
diff --git a/tests/e2e/enums/equations.ts b/tests/e2e/enums/equations.ts
new file mode 100644
index 000000000..71c68c796
--- /dev/null
+++ b/tests/e2e/enums/equations.ts
@@ -0,0 +1,36 @@
+import type Equation from '../interfaces/equation'
+
+const Equations: Record = {
+ singleNumber: {
+ altText: '1',
+ mathml: ''
+ },
+ styledSingleNumber: {
+ altText: 'bold italic 1',
+ mathml: ''
+ },
+ squareRootY: {
+ altText: 'square root of y',
+ mathml: '',
+ latex: '\\sqrt{y}'
+ },
+ squareRootYPlusFive: {
+ altText: 'square root of y plus 5',
+ mathml: '',
+ latex: '\\sqrt y+5'
+ },
+ OnePlusOne: {
+ altText: '1 plus 1',
+ mathml: ''
+ },
+ styledOnePlusOne: {
+ altText: 'bold italic 1 bold italic plus bold italic 1',
+ mathml: ''
+ },
+ specialCharacters: {
+ altText: '« less than » greater than § & ¨ " apostrophe apostrophe',
+ mathml: ''
+ }
+}
+
+export default Equations
diff --git a/tests/e2e/enums/toolbar.ts b/tests/e2e/enums/toolbar.ts
new file mode 100644
index 000000000..5ed6fad4d
--- /dev/null
+++ b/tests/e2e/enums/toolbar.ts
@@ -0,0 +1,6 @@
+enum Toolbar {
+ MATH = 'MathType',
+ CHEMISTRY = 'ChemType',
+}
+
+export default Toolbar
\ No newline at end of file
diff --git a/tests/e2e/enums/typing_mode.ts b/tests/e2e/enums/typing_mode.ts
new file mode 100644
index 000000000..3eaa5bd5b
--- /dev/null
+++ b/tests/e2e/enums/typing_mode.ts
@@ -0,0 +1,7 @@
+enum TypingMode {
+ KEYBOARD = 'KEY',
+ HAND = 'HAND',
+ UNKNOWN = 'UNKNOWN',
+}
+
+export default TypingMode
\ No newline at end of file
diff --git a/tests/e2e/helpers/network.ts b/tests/e2e/helpers/network.ts
new file mode 100644
index 000000000..be0d463d5
--- /dev/null
+++ b/tests/e2e/helpers/network.ts
@@ -0,0 +1,20 @@
+import { Page } from '@playwright/test'
+
+export const captureTelemetryRequests = async (page: Page, foundEvents: string[]): Promise => {
+ // Enable request interception and handle requests
+ page.on('request', (request) => {
+ if (request.method() === 'POST' && request.url().includes('telemetry')) {
+ const requestData = request.postData()
+ if (requestData) {
+ const eventCaptured = getTelemetryEventCaptured(requestData)
+ foundEvents.push(eventCaptured)
+ }
+ }
+ })
+}
+
+export const getTelemetryEventCaptured = (requestData: string): string => {
+ const requestDataJSON = JSON.parse(requestData)
+ const eventCaptured: string = requestDataJSON.evs[0].typ
+ return eventCaptured
+}
\ No newline at end of file
diff --git a/tests/e2e/helpers/test-setup.ts b/tests/e2e/helpers/test-setup.ts
new file mode 100644
index 000000000..0acaa1936
--- /dev/null
+++ b/tests/e2e/helpers/test-setup.ts
@@ -0,0 +1,30 @@
+import { Page } from '@playwright/test'
+import EditorManager from '../page-objects/editor_manager'
+import WirisEditor from '../page-objects/wiris_editor'
+import BaseEditor from '../page-objects/base_editor'
+
+export interface TestSetup {
+ editor: BaseEditor
+ wirisEditor: WirisEditor
+}
+
+export async function setupEditor(page: Page, editorName: string): Promise {
+ const editorManager = new EditorManager()
+ const availableEditors = editorManager.getEditors(page)
+ const editor = availableEditors.find(e => e.getName() === editorName)
+
+ if (!editor) {
+ throw new Error(`Editor ${editorName} not found`)
+ }
+
+ const wirisEditor = new WirisEditor(page)
+
+ return { editor, wirisEditor }
+}
+
+export function getEditorsFromEnv(): string[] {
+ if (!process.env.HTML_EDITOR) {
+ throw new Error('Environment variable HTML_EDITOR is not set')
+ }
+ return process.env.HTML_EDITOR.split('|')
+}
diff --git a/tests/e2e/interfaces/equation.ts b/tests/e2e/interfaces/equation.ts
new file mode 100644
index 000000000..48c4d85f9
--- /dev/null
+++ b/tests/e2e/interfaces/equation.ts
@@ -0,0 +1,5 @@
+export default interface Equation {
+ altText: string
+ mathml: string
+ latex?: string
+}
\ No newline at end of file
diff --git a/tests/e2e/page-objects/base_editor.ts b/tests/e2e/page-objects/base_editor.ts
new file mode 100644
index 000000000..40c852663
--- /dev/null
+++ b/tests/e2e/page-objects/base_editor.ts
@@ -0,0 +1,480 @@
+import { Page, Locator, expect, FrameLocator } from '@playwright/test'
+import Toolbar from '../enums/toolbar'
+import type Equation from '../interfaces/equation'
+import BasePage from './page'
+const path = require('path')
+
+/**
+ * Abstract class used in each of the HTML editors which includes the methods for all the editors, and specifies the properties each editor needs.
+ */
+export default abstract class BaseEditor extends BasePage {
+ protected readonly abstract wirisEditorButtonMathType: string
+ protected readonly abstract wirisEditorButtonChemType: string
+ protected readonly abstract name: string
+ protected readonly abstract editField: string
+ protected readonly iframe?: string
+
+ constructor(page: Page) {
+ super(page)
+ }
+
+ public getName(): string {
+ return this.name
+ }
+
+ public getIframe(): string | undefined {
+ return this.iframe
+ }
+
+ /**
+ * Constructs the URL for the specific editor and opens it in the browser.
+ * @returns {Promise} The URL of the opened editor. **/
+ public async open(): Promise {
+ const isStaging = process.env.USE_STAGING === 'true'
+
+ let url: string
+
+ // Determine the URL based on the environment (staging or local) and the html editor name
+ if (isStaging) {
+ url = `${process.env.TEST_BRANCH}/html/${this.getName()}/`
+ } else {
+ // Use localhost with each editor's corresponding port
+ const editorPortMap = {
+ 'ckeditor4': 8001,
+ 'ckeditor5': 8002,
+ 'froala': 8004,
+ 'tinymce5': 8006,
+ 'tinymce6': 8008,
+ 'tinymce7': 8009,
+ 'tinymce8': 8010,
+ 'generic': 8007,
+ }
+
+ const port = editorPortMap[this.getName() as keyof typeof editorPortMap]
+ if (!port) {
+ throw new Error(`No port mapping found for editor: ${this.getName()}`)
+ }
+
+ url = `http://localhost:${port}/`
+ }
+
+ await this.page.goto(url)
+ await this.page.waitForLoadState('domcontentloaded')
+ return this.page.url()
+ }
+
+ /**
+ * Opens the Wiris editor based on a provided toolbar type.
+ * @param {Toolbar} toolbar - The type of the toolbar to open, either Math or Chemistry.
+ */
+ public async openWirisEditor(toolbar: Toolbar): Promise {
+ switch (toolbar) {
+ case Toolbar.CHEMISTRY:
+ await this.page.locator(this.wirisEditorButtonChemType).waitFor({ state: 'visible' })
+ await this.page.locator(this.wirisEditorButtonChemType).hover()
+ await this.page.locator(this.wirisEditorButtonChemType).click()
+ break
+ default:
+
+ await this.page.locator(this.wirisEditorButtonMathType).waitFor({ state: 'visible' })
+ await this.page.locator(this.wirisEditorButtonMathType).hover()
+ await this.page.locator(this.wirisEditorButtonMathType).click()
+ break
+ }
+ }
+
+ /**
+ * Opens the source code editor
+ */
+ public async clickSourceCodeEditor(): Promise {
+ const sourceCodeButton = this.getSourceCodeEditorButton?.()
+ if (sourceCodeButton) {
+ await this.page.locator(sourceCodeButton).waitFor({ state: 'visible' })
+ await this.page.locator(sourceCodeButton).click()
+ }
+ }
+
+ public async typeSourceText(text: string): Promise {
+ const sourceCodeEditField = this.getSourceCodeEditField?.()
+ if (!sourceCodeEditField) {
+ throw new Error('Source code edit field selector is not defined.')
+ }
+ await this.page.locator(sourceCodeEditField).waitFor({ state: 'visible' })
+ await this.page.locator(sourceCodeEditField).click()
+ await this.pause(500)
+ await this.page.keyboard.type(text)
+ }
+
+ /**
+ * Retrieves all equations from the editor using the alt text and data-mathml DOM attributes.
+ * @returns {Promise} Array of equation interface.
+ */
+ public async getEquations(): Promise {
+ let frameOrPage: Page | FrameLocator
+ if (this.iframe) {
+ frameOrPage = this.page.frameLocator(this.iframe)
+ } else {
+ frameOrPage = this.page
+ }
+
+ await this.page.waitForTimeout(500)
+
+ const equationsInEditor = await frameOrPage.locator(`${this.editField} img[alt][data-mathml]`)
+ const count = await equationsInEditor.count()
+ const equations: Equation[] = []
+
+ for (let i = 0; i < count; i++) {
+ const equation = equationsInEditor.nth(i)
+ const altText = await equation.getAttribute('alt') || ''
+ const mathml = await equation.getAttribute('data-mathml') || ''
+ equations.push({ altText, mathml })
+ }
+
+ return equations
+ }
+
+ /**
+ * Waits for a specific equation to appear in the editor.
+ * @param {Equation} equation - The equation to wait for.
+ */
+ public async waitForEquation(equation: Equation): Promise {
+ await expect(async () => {
+ const equations = await this.getEquations()
+ expect(equations.some((eq) => eq.altText === equation.altText)).toBeTruthy()
+ }).toPass({ timeout: 10000 })
+ }
+
+ /**
+ * Gets a locator for the equation element within the DOM.
+ * @param {Equation} equation - The equation to find in the DOM.
+ * @returns {Locator} The Playwright locator representing the equation.
+ */
+ public getEquationElement(equation: Equation): Locator {
+ if (this.iframe) {
+ return this.page.frameLocator(this.iframe).locator(`${this.editField} img[alt="${equation.altText}"]`)
+ }
+
+ return this.page.locator(`${this.editField} img[alt="${equation.altText}"]`)
+ }
+
+ /**
+ * Clicks on an element for a specified number of times. The reason for this function is to handle the iframe switching. Used for elements belonging to the editor.
+ * @param {Locator} elementToClick - The element to click on.
+ * @param {number} [numberOfTimes=1] - The number of times to click on the element.
+ */
+ public async clickElement(elementToClick: Locator, numberOfTimes: number = 1): Promise {
+ switch (numberOfTimes) {
+ case 1:
+ await elementToClick.click()
+ break
+ case 2:
+ await elementToClick.dblclick()
+ break
+ default:
+ for (let i = 0; i < numberOfTimes; i++) {
+ await elementToClick.click()
+ await this.pause(500)
+ }
+ break
+ }
+ }
+
+ /**
+ * Focuses the editing field within the editor.
+ */
+ public async focus(): Promise {
+ let editFieldLocator: Locator
+
+ if (this.iframe) {
+ editFieldLocator = this.page.frameLocator(this.iframe).locator(this.editField)
+ } else {
+ editFieldLocator = this.page.locator(this.editField)
+ }
+ await editFieldLocator.click()
+ await this.pause(1000)
+ }
+
+ /**
+ * Appends text at the bottom of the editor field. This uses the keyboard to go to the end of the edit field and append.
+ * @param {string} textToInsert - The text to append.
+ */
+ public async appendText(textToInsert: string): Promise {
+ await this.focus()
+
+ await this.page.keyboard.press('Control+End')
+ await this.pause(500)
+ await this.page.keyboard.type(textToInsert)
+ }
+
+ /**
+ * Open the wiris Editor to edit the last item inserted
+ * Uses selectItemAtCursor, but that's not compatible with froala, so in that case does a click in the contextual toolbar
+ * @param {Toolbar} toolbar - toolbar of the test
+ */
+ public async openWirisEditorForLastInsertedFormula(toolbar: Toolbar, equation: Equation): Promise {
+ const isFroala = this.getName() === 'froala'
+
+ if (isFroala) {
+ const equationElement = this.getEquationElement(equation)
+ await equationElement.click()
+
+ const mathTypeButton = this.getContextualToolbarMathTypeButton?.()
+ if (mathTypeButton) {
+ await this.page.locator(mathTypeButton).click()
+ }
+ } else {
+ await this.selectItemAtCursor()
+ await this.openWirisEditor(toolbar)
+ }
+ }
+
+ /**
+ * Selects the item at the current cursor position within the editor. This uses shift + the left arrow key to select.
+ */
+ public async selectItemAtCursor(): Promise {
+ await this.page.keyboard.press('Shift+ArrowLeft')
+ }
+
+ /**
+ * This gets all the text in the editor field
+ * @returns boolean indicating if text appears after equation
+ */
+ public async isTextAfterEquation(typedText: string, altTextEquation: string): Promise {
+ let frameOrPage: Page | FrameLocator
+ if (this.iframe) {
+ frameOrPage = this.page.frameLocator(this.iframe)
+ } else {
+ frameOrPage = this.page
+ }
+
+ const html = await frameOrPage.locator(this.editField).innerHTML()
+ const indexText = html.indexOf(typedText)
+ const indexEquation = html.indexOf(altTextEquation)
+
+ return indexEquation < indexText
+ }
+
+ /**
+ * This gets all the latex equations $$ expression $$ from the edit field
+ * It then trims whitespaces at beginning and end,
+ * and replaces instances of $$ for blank text so as to get only the latex.
+ * @returns an array of strings containing latex equations or undefined if there are none
+ */
+ public async getLatexEquationsInEditField(): Promise {
+ let frameOrPage: Page | FrameLocator
+ if (this.iframe) {
+ frameOrPage = this.page.frameLocator(this.iframe)
+ } else {
+ frameOrPage = this.page
+ }
+
+ const textContents = await frameOrPage.locator(this.editField).textContent()
+
+ if (!textContents) {
+ return undefined
+ }
+
+ const expressions = textContents.match(/\$\$.*?\$\$/g)?.map((latexEquation) => latexEquation.trim().replaceAll('$$', ''))
+
+ return expressions
+ }
+
+ public async waitForLatexExpression(latexExpression: string): Promise {
+ await expect(async () => {
+ const latexEquations = await this.getLatexEquationsInEditField()
+ expect(latexEquations?.some((eq: string) => eq === latexExpression)).toBeTruthy()
+ }).toPass({ timeout: 10000 })
+ }
+
+ public async copyAllEditorContent(): Promise {
+ await this.focus()
+ await this.page.keyboard.press('Control+a')
+ await this.setClipboardText('')
+ await this.pause(500)
+ await this.page.keyboard.press('Control+c')
+ }
+
+ public async cutAllEditorContent(): Promise {
+ await this.focus()
+ await this.page.keyboard.press('Control+a')
+ await this.setClipboardText('')
+ await this.pause(500)
+ await this.page.keyboard.press('Control+x')
+ }
+
+ async setClipboardText(text: string): Promise {
+ await this.page.evaluate(async (t) => {
+ await (globalThis as any).navigator.clipboard.writeText(t);
+ }, text);
+ }
+
+ public async dragDropLastFormula(equation: Equation): Promise {
+ await this.focus()
+
+ const equationElement = this.getEquationElement(equation)
+ let editDivElement: Locator
+
+ if (this.iframe) {
+ editDivElement = this.page.frameLocator(this.iframe).locator(this.editField)
+ } else {
+ editDivElement = this.page.locator(this.editField)
+ }
+
+ const equationBox = await equationElement.boundingBox()
+ const editDivBox = await editDivElement.boundingBox()
+
+ if (equationBox && editDivBox) {
+ await this.page.mouse.move(equationBox.x + equationBox.width / 2, equationBox.y + equationBox.height / 2)
+ //await this.page.mouse.click(equationBox.x + equationBox.width / 2, equationBox.y + equationBox.height / 2)
+ await this.pause(500)
+ await this.page.mouse.down()
+ await this.pause(500)
+ await this.page.mouse.move(editDivBox.x, editDivBox.y)
+ await this.pause(500)
+ await this.page.mouse.up()
+ }
+ }
+
+ public async paste(): Promise {
+ await this.focus()
+ await this.page.keyboard.press('Control+v')
+ }
+
+ public async undo(): Promise {
+ await this.focus()
+ await this.page.keyboard.press('Control+z')
+ }
+
+ public async redo(): Promise {
+ await this.focus()
+ await this.page.keyboard.press('Control+Shift+z')
+ }
+
+ public async clear(): Promise {
+ await this.focus()
+ if (this.iframe) { await this.focus() } // avoids failing to clear in ckeditor4 if not focused
+ await this.page.keyboard.press('Control+a')
+ await this.pause(500)
+ await this.page.keyboard.press('Delete')
+ }
+
+ public async isEditorCleared(): Promise {
+ let frameOrPage: Page | FrameLocator
+ if (this.iframe) {
+ frameOrPage = this.page.frameLocator(this.iframe)
+ } else {
+ frameOrPage = this.page
+ }
+
+ await this.pause(1000)
+
+ const element = frameOrPage.locator(this.editField)
+ const rawText = (await element.textContent()) ?? ''
+ const normalized = rawText.replace(/[\s\uFEFF\xA0]+/g, '')
+ const noTextInEditor = normalized === ''
+ const equationElements = frameOrPage.locator(`${this.editField} img`)
+ const noEquationsInEditor = (await equationElements.count()) === 0
+
+ return noEquationsInEditor && noTextInEditor
+ }
+
+ public async getImageSize(equation: Equation): Promise<{ width: number; height: number } | null> {
+ await this.focus()
+
+ const equationElement = this.getEquationElement(equation)
+ return await equationElement.boundingBox()
+ }
+
+ public async resizeImageEquation(equation: Equation): Promise {
+ await this.focus()
+
+ const equationElement = this.getEquationElement(equation)
+ await equationElement.click()
+
+ const box = await equationElement.boundingBox()
+ if (box) {
+ await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
+ await this.pause(500)
+ await this.page.mouse.down()
+ await this.pause(500)
+ await this.page.mouse.move(box.x - 10, box.y - 10)
+ await this.pause(500)
+ await this.page.mouse.up()
+ }
+ }
+
+ public async applyStyle(): Promise {
+ await this.focus()
+ await this.page.keyboard.press('Control+b') // Bold
+ await this.pause(500)
+ await this.page.keyboard.press('Control+i') // Italic
+ }
+
+ public async isTextBoldAndItalic(text: string): Promise {
+ await this.focus()
+
+ let frameOrPage: Page | FrameLocator
+ if (this.iframe) {
+ frameOrPage = this.page.frameLocator(this.iframe)
+ } else {
+ frameOrPage = this.page
+ }
+
+ const isCkeditor5 = this.getName() === 'ckeditor5'
+ if (isCkeditor5) {
+ await this.page.keyboard.press('Enter')
+ await this.pause(500)
+ await this.page.keyboard.press('Backspace')
+ }
+
+ const elements = frameOrPage.locator(`${this.editField} >> text="${text}"`)
+ const count = await elements.count()
+
+ for (let i = 0; i < count; i++) {
+ const element = elements.nth(i)
+ const fontWeight = await element.evaluate((el) => (globalThis as any).getComputedStyle(el).fontWeight)
+ const fontStyle = await element.evaluate((el) => (globalThis as any).getComputedStyle(el).fontStyle)
+
+ const isBold = parseInt(fontWeight) >= 700
+ const isItalic = fontStyle === 'italic'
+
+ if (isBold && isItalic) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ public async moveCaret(): Promise {
+ await this.focus()
+ for (let i = 0; i < 8; i++) {
+ await this.page.keyboard.press('ArrowLeft')
+ }
+ }
+
+ public async checkElementAlignment(): Promise {
+ await this.focus()
+
+ let editContent: Locator
+
+ if (this.iframe) {
+ editContent = this.page.frameLocator(this.iframe).locator(this.editField)
+ } else {
+ editContent = this.page.locator(this.editField)
+ }
+
+ // Take screenshot for visual comparison
+ await editContent.screenshot({
+ path: path.resolve(__dirname, '../screenshots', `${this.getName()}_alignment.png`)
+ })
+ }
+
+ public getContextualToolbarMathTypeButton?(): string
+
+ public getContextualToolbarChemTypeButton?(): string
+
+ public getSourceCodeEditorButton?(): string
+
+ public getSourceCodeEditField?(): string
+}
diff --git a/tests/e2e/page-objects/editor_manager.ts b/tests/e2e/page-objects/editor_manager.ts
new file mode 100644
index 000000000..7208b5731
--- /dev/null
+++ b/tests/e2e/page-objects/editor_manager.ts
@@ -0,0 +1,42 @@
+import { Page } from '@playwright/test'
+import Generic from './html/generic'
+import CKEditor5 from './html/ckeditor5'
+import CKEditor4 from './html/ckeditor4'
+import Froala from './html/froala'
+import TinyMCE5 from './html/tinymce5'
+import TinyMCE6 from './html/tinymce6'
+import TinyMCE7 from './html/tinymce7'
+import TinyMCE8 from './html/tinymce8'
+
+import type BaseEditor from './base_editor'
+
+class EditorManager {
+ private editorsInConfiguration: string[] | undefined
+
+ public getEditors(page: Page): BaseEditor[] {
+
+ const availableEditors: BaseEditor[] = [
+ new Generic(page),
+ new CKEditor5(page),
+ new CKEditor4(page),
+ new Froala(page),
+ new TinyMCE5(page),
+ new TinyMCE6(page),
+ new TinyMCE7(page),
+ new TinyMCE8(page),
+ ]
+
+ this.editorsInConfiguration = process.env.HTML_EDITOR?.split('|')
+ const editorsToUse = availableEditors.filter((editor) =>
+ ((this.editorsInConfiguration?.includes(editor.getName())) ?? false)
+ )
+
+ if (editorsToUse.length === 0) {
+ throw new Error(`No valid editors found in current configuration: ${process.env.HTML_EDITOR}`)
+ }
+
+ return editorsToUse
+ }
+}
+
+export default EditorManager
\ No newline at end of file
diff --git a/tests/e2e/page-objects/equation_entry_form.ts b/tests/e2e/page-objects/equation_entry_form.ts
new file mode 100644
index 000000000..1d719fc3a
--- /dev/null
+++ b/tests/e2e/page-objects/equation_entry_form.ts
@@ -0,0 +1,46 @@
+import { Page, Locator, expect } from '@playwright/test'
+import BasePage from './page'
+
+class EquationEntryForm extends BasePage {
+ constructor(page: Page) {
+ super(page)
+ }
+
+ get editField(): Locator {
+ return this.page.locator('textarea')
+ }
+
+ get submitButton(): Locator {
+ return this.page.locator('input[type="submit"]')
+ }
+
+ public async isOpen(): Promise {
+ const context = this.page.context()
+ const pages = context.pages()
+ if (pages.length !== 2) return false;
+ const editAreaVisible = await this.editField.isVisible()
+ const submitButtonVisible = await this.submitButton.isVisible()
+ return submitButtonVisible && editAreaVisible
+ }
+
+ public async setEquation(text: string): Promise {
+ await this.editField.fill(text)
+ const closePromise = this.page.waitForEvent('close')
+ await this.submitButton.click({ force: true }).catch(() => {}) // Ignore any errors from closing the popup
+ await closePromise
+ }
+
+ public async getText(): Promise {
+ await expect(async () => {
+ const text = await this.editField.inputValue()
+ expect(text).not.toBe('')
+ }).toPass({ timeout: 5000 })
+
+ const text = await this.editField.inputValue()
+ await this.submitButton.click()
+
+ return text
+ }
+}
+
+export default EquationEntryForm
\ No newline at end of file
diff --git a/tests/e2e/page-objects/html/ckeditor4.ts b/tests/e2e/page-objects/html/ckeditor4.ts
new file mode 100644
index 000000000..a5d9a65e6
--- /dev/null
+++ b/tests/e2e/page-objects/html/ckeditor4.ts
@@ -0,0 +1,16 @@
+import { Page } from '@playwright/test'
+import BaseEditor from '../base_editor'
+
+class CKEditor4 extends BaseEditor {
+ protected readonly wirisEditorButtonMathType = "[title='Insert a math equation - MathType']"
+ protected readonly wirisEditorButtonChemType = "[title='Insert a chemistry formula - ChemType']"
+ protected readonly editField = 'body'
+ protected readonly iframe = "iframe[title='Editor, editor']"
+ protected readonly name = 'ckeditor4'
+
+ constructor(page: Page) {
+ super(page)
+ }
+}
+
+export default CKEditor4
\ No newline at end of file
diff --git a/tests/e2e/page-objects/html/ckeditor5.ts b/tests/e2e/page-objects/html/ckeditor5.ts
new file mode 100644
index 000000000..a098a3164
--- /dev/null
+++ b/tests/e2e/page-objects/html/ckeditor5.ts
@@ -0,0 +1,25 @@
+import { Page } from '@playwright/test'
+import BaseEditor from '../base_editor'
+
+class CKEditor5 extends BaseEditor {
+ protected readonly wirisEditorButtonMathType = "[data-cke-tooltip-text='Insert a math equation - MathType']"
+ protected readonly wirisEditorButtonChemType = "[data-cke-tooltip-text='Insert a chemistry formula - ChemType']"
+ protected readonly sourceCodeEditorButton = "[data-cke-tooltip-text='Source']"
+ protected readonly sourceCodeEditField = '.ck-source-editing-area'
+ protected readonly editField = '.ck-editor__editable'
+ protected readonly name = 'ckeditor5'
+
+ constructor(page: Page) {
+ super(page)
+ }
+
+ public getSourceCodeEditorButton(): string {
+ return this.sourceCodeEditorButton
+ }
+
+ public getSourceCodeEditField(): string {
+ return this.sourceCodeEditField
+ }
+}
+
+export default CKEditor5
\ No newline at end of file
diff --git a/tests/e2e/page-objects/html/froala.ts b/tests/e2e/page-objects/html/froala.ts
new file mode 100644
index 000000000..fffca1ad5
--- /dev/null
+++ b/tests/e2e/page-objects/html/froala.ts
@@ -0,0 +1,23 @@
+import { Page } from '@playwright/test'
+import BaseEditor from '../base_editor'
+
+class Froala extends BaseEditor {
+ protected readonly wirisEditorButtonMathType = '.fr-btn-grp.fr-float-left >> #wirisEditor-1'
+ protected readonly wirisEditorButtonChemType = '.fr-btn-grp.fr-float-left >> #wirisChemistry-1'
+ protected readonly editField = '.fr-element'
+ protected readonly name = 'froala'
+
+ constructor(page: Page) {
+ super(page)
+ }
+
+ public getContextualToolbarMathTypeButton(): string {
+ return '.fr-buttons #wirisEditor-1'
+ }
+
+ public getContextualToolbarChemTypeButton(): string {
+ return '.fr-buttons #wirisChemistry-1'
+ }
+}
+
+export default Froala
\ No newline at end of file
diff --git a/tests/e2e/page-objects/html/generic.ts b/tests/e2e/page-objects/html/generic.ts
new file mode 100644
index 000000000..d8c72f686
--- /dev/null
+++ b/tests/e2e/page-objects/html/generic.ts
@@ -0,0 +1,15 @@
+import { Page } from '@playwright/test'
+import BaseEditor from '../base_editor'
+
+class Generic extends BaseEditor {
+ protected readonly wirisEditorButtonChemType = '#chemistryIcon'
+ protected readonly wirisEditorButtonMathType = '#editorIcon'
+ protected readonly editField = '#editable'
+ protected readonly name = 'generic'
+
+ constructor(page: Page) {
+ super(page)
+ }
+}
+
+export default Generic
\ No newline at end of file
diff --git a/tests/e2e/page-objects/html/tinymce5.ts b/tests/e2e/page-objects/html/tinymce5.ts
new file mode 100644
index 000000000..0359f183a
--- /dev/null
+++ b/tests/e2e/page-objects/html/tinymce5.ts
@@ -0,0 +1,16 @@
+import { Page } from '@playwright/test'
+import BaseEditor from '../base_editor'
+
+class TinyMCE5 extends BaseEditor {
+ protected readonly wirisEditorButtonChemType = "[aria-label='Insert a chemistry formula - ChemType']"
+ protected readonly wirisEditorButtonMathType = "[aria-label='Insert a math equation - MathType']"
+ protected readonly editField = 'body'
+ protected readonly iframe = "iframe[id='editor_ifr']"
+ protected readonly name = 'tinymce5'
+
+ constructor(page: Page) {
+ super(page)
+ }
+}
+
+export default TinyMCE5
\ No newline at end of file
diff --git a/tests/e2e/page-objects/html/tinymce6.ts b/tests/e2e/page-objects/html/tinymce6.ts
new file mode 100644
index 000000000..6d3d21259
--- /dev/null
+++ b/tests/e2e/page-objects/html/tinymce6.ts
@@ -0,0 +1,16 @@
+import { Page } from '@playwright/test'
+import BaseEditor from '../base_editor'
+
+class TinyMCE6 extends BaseEditor {
+ protected readonly wirisEditorButtonChemType = "[aria-label='Insert a chemistry formula - ChemType']"
+ protected readonly wirisEditorButtonMathType = "[aria-label='Insert a math equation - MathType']"
+ protected readonly editField = 'body'
+ protected readonly iframe = "iframe[id='editor_ifr']"
+ protected readonly name = 'tinymce6'
+
+ constructor(page: Page) {
+ super(page)
+ }
+}
+
+export default TinyMCE6
\ No newline at end of file
diff --git a/tests/e2e/page-objects/html/tinymce7.ts b/tests/e2e/page-objects/html/tinymce7.ts
new file mode 100644
index 000000000..5cc845559
--- /dev/null
+++ b/tests/e2e/page-objects/html/tinymce7.ts
@@ -0,0 +1,16 @@
+import { Page } from '@playwright/test'
+import BaseEditor from '../base_editor'
+
+class TinyMCE7 extends BaseEditor {
+ protected readonly wirisEditorButtonChemType = "[aria-label='Insert a chemistry formula - ChemType']"
+ protected readonly wirisEditorButtonMathType = "[aria-label='Insert a math equation - MathType']"
+ protected readonly editField = 'body'
+ protected readonly iframe = "iframe[id='editor_ifr']"
+ protected readonly name = 'tinymce7'
+
+ constructor(page: Page) {
+ super(page)
+ }
+}
+
+export default TinyMCE7
\ No newline at end of file
diff --git a/tests/e2e/page-objects/html/tinymce8.ts b/tests/e2e/page-objects/html/tinymce8.ts
new file mode 100644
index 000000000..a2e41d938
--- /dev/null
+++ b/tests/e2e/page-objects/html/tinymce8.ts
@@ -0,0 +1,16 @@
+import { Page } from '@playwright/test'
+import BaseEditor from '../base_editor'
+
+class TinyMCE8 extends BaseEditor {
+ protected readonly wirisEditorButtonChemType = "[aria-label='Insert a chemistry formula - ChemType']"
+ protected readonly wirisEditorButtonMathType = "[aria-label='Insert a math equation - MathType']"
+ protected readonly editField = 'body'
+ protected readonly iframe = "iframe[id='editor_ifr']"
+ protected readonly name = 'tinymce8'
+
+ constructor(page: Page) {
+ super(page)
+ }
+}
+
+export default TinyMCE8
\ No newline at end of file
diff --git a/tests/e2e/page-objects/page.ts b/tests/e2e/page-objects/page.ts
new file mode 100644
index 000000000..563b3bba2
--- /dev/null
+++ b/tests/e2e/page-objects/page.ts
@@ -0,0 +1,16 @@
+import { Page } from '@playwright/test'
+
+export default class BasePage {
+ protected page: Page
+
+ constructor(page: Page) {
+ this.page = page
+ }
+
+ /**
+ * Wait for a specific amount of time
+ */
+ async pause(milliseconds: number): Promise {
+ await this.page.waitForTimeout(milliseconds)
+ }
+}
diff --git a/tests/e2e/page-objects/wiris_editor.ts b/tests/e2e/page-objects/wiris_editor.ts
new file mode 100644
index 000000000..436f0e60e
--- /dev/null
+++ b/tests/e2e/page-objects/wiris_editor.ts
@@ -0,0 +1,213 @@
+import { Page, Locator, expect } from '@playwright/test'
+import BasePage from './page'
+import EquationEntryMode from '../enums/equation_entry_mode'
+import TypingMode from '../enums/typing_mode'
+import EquationEntryForm from './equation_entry_form'
+
+class WirisEditor extends BasePage {
+ private equationEntryForm: EquationEntryForm
+
+ constructor(page: Page) {
+ super(page)
+ this.equationEntryForm = new EquationEntryForm(page)
+ }
+
+ get modalTitle(): Locator {
+ return this.page.locator('.wrs_modal_title')
+ }
+
+ get wirisEditorWindow(): Locator {
+ return this.page.locator('.wrs_content_container')
+ }
+
+ get insertButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-insert-button']")
+ }
+
+ get cancelButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-cancel-button']")
+ }
+
+ get handModeButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-key2hand-button']")
+ }
+
+ get closeButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-close-button']")
+ }
+
+ get fullScreenButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-fullscreen-enable-button']")
+ }
+
+ get exitFullScreenButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-minimize-button']")
+ }
+
+ get modalOverlay(): Locator {
+ return this.page.locator('[id*=wrs_modal_overlay]')
+ }
+
+ get minimizeButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-fullscreen-disable-button']")
+ }
+
+ get mathInputField(): Locator {
+ return this.page.locator('input[aria-label="Math input"]')
+ }
+
+ get confirmationDialog(): Locator {
+ return this.page.locator('#wrs_popupmessage')
+ }
+
+ get confirmationDialogCancelButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-cd-cancel-button']")
+ }
+
+ get confirmationDialogCloseButton(): Locator {
+ return this.page.locator("[data-testid='mtcteditor-cd-close-button']")
+ }
+
+ get handCanvas(): Locator {
+ return this.page.locator('canvas.wrs_canvas')
+ }
+
+ /**
+ * Checks if the wiris editor modal is open by checking for the presence of the modal window, cancel and insert buttons.
+ */
+ public async isOpen(): Promise {
+ return (await this.insertButton.isVisible()) && (await this.closeButton.isVisible() && (await this.wirisEditorWindow.isVisible()))
+ }
+
+ /**
+ * This waits for the wiris editor modal to be open and for the cancel button to be displayed
+ */
+ public async waitUntilLoaded(typeMode?: TypingMode): Promise {
+ typeMode = typeMode ?? TypingMode.KEYBOARD
+ await this.wirisEditorWindow.waitFor({ state: 'visible' })
+ await this.insertButton.waitFor({ state: 'visible' })
+ await this.handModeButton.waitFor({ state: 'visible' })
+ await this.cancelButton.waitFor({ state: 'visible' })
+ if (typeMode === TypingMode.KEYBOARD) {
+ await this.mathInputField.waitFor({ state: 'visible' })
+ } else {
+ await this.handCanvas.waitFor({ state: 'visible' })
+ }
+ }
+
+ /**
+ * This waits for the wiris editor modal to be closed
+ */
+ public async waitUntilClosed(): Promise {
+ await this.wirisEditorWindow.waitFor({ state: 'hidden' })
+ }
+
+ /**
+ * This performs select all and delete using Playwright keyboard, cross platform
+ */
+ public async deleteContents(): Promise {
+ await this.page.keyboard.press('Control+a')
+ await this.page.keyboard.press('Delete')
+ }
+
+ /**
+ * This types an equation into the edit field
+ * @param keysToType This parameter specifies which keys will be typed in the editor.
+ */
+ public async typeEquationViaKeyboard(keysToType: string): Promise {
+ await this.mathInputField.click()
+ await this.pause(500) // This wait is needed in order to simulate real typing
+ await this.page.keyboard.type(keysToType)
+ await this.pause(500) // If we don't wait, it crashes. This is typical also with a user that would type, wait a few seconds, then insert equation.
+ }
+
+ /**
+ * The same as typeEquation, but this inserts the equation
+ */
+ public async insertEquationViaKeyboard(keysToType: string): Promise {
+ await this.typeEquationViaKeyboard(keysToType)
+ await this.insertButton.click()
+ await this.waitUntilClosed()
+ }
+
+ /**
+ * @param entryMode (EquationEntryMode): This parameter specifies which mode the equation entry form will be opened in.
+ * This presses ctrl shift X for MathML and L for latex to show the equation entry form.
+ */
+ public async showEquationEntryForm(entryMode: EquationEntryMode): Promise {
+ const popupPromise = this.page.waitForEvent('popup');
+ switch (entryMode) {
+ case EquationEntryMode.MATHML:
+ await this.page.keyboard.press('Control+Shift+KeyX')
+ break
+ case EquationEntryMode.LATEX:
+ await this.page.keyboard.press('Control+Shift+KeyL')
+ break
+ }
+ const popup = await popupPromise;
+ this.equationEntryForm = new EquationEntryForm(popup);
+ expect(await this.equationEntryForm.isOpen()).toBeTruthy()
+ }
+
+ /**
+ * This allows insertion of an equation by typing text into equation entry form to insert the equation. and automatically hit the insert button
+ * @param text the text to type into the form
+ * If the equation entry form is not open, it will open by default using MathML mode.
+ */
+ public async insertEquationUsingEntryForm(text: string): Promise {
+ await this.typeEquationUsingEntryForm(text)
+ await this.pause(500) // TODO: Used to avoid click insert without formula submit, should be investigated further
+ await this.insertButton.click()
+ await this.waitUntilClosed()
+ }
+
+ /**
+ * This allows insertion of an equation by typing text into equation entry form to insert the equation. It does not hit the insert button.
+ * @param text the text to type into the form
+ * If the equation entry form is not open, it will open by default using MathML mode.
+ */
+ public async typeEquationUsingEntryForm(text: string): Promise {
+ const entryFormVisible = await this.equationEntryForm.isOpen()
+
+ if (!entryFormVisible) {
+ await this.showEquationEntryForm(EquationEntryMode.MATHML)
+ }
+
+ await this.equationEntryForm.setEquation(text)
+ }
+
+ /**
+ * This uses the equation entry form to get the MathML currently being used in the editor.
+ * @returns a string with the MathML currently used in the rendered equation.
+ */
+ public async getMathML(): Promise {
+ const entryFormVisible = await this.equationEntryForm.isOpen()
+
+ if (!entryFormVisible) {
+ await this.showEquationEntryForm(EquationEntryMode.MATHML)
+ }
+
+ return await this.equationEntryForm.getText()
+ }
+
+ /**
+ * Appends text at the bottom of the math input field. This uses the keyboard to go to the end of the edit field and append.
+ * @param {string} textToInsert - The text to append.
+ */
+ public async appendText(textToInsert: string): Promise {
+ await this.mathInputField.click()
+ await this.pause(500)
+ await this.page.keyboard.press('End')
+ await this.pause(500)
+ await this.page.keyboard.type(textToInsert)
+ }
+
+ public async getMode(): Promise {
+ const title = await this.handModeButton.getAttribute('title')
+ return title === 'Go to handwritten mode' ? TypingMode.KEYBOARD :
+ title === 'Use keyboard' ? TypingMode.HAND :
+ TypingMode.UNKNOWN
+ }
+}
+
+export default WirisEditor
\ No newline at end of file
diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts
new file mode 100644
index 000000000..04c386927
--- /dev/null
+++ b/tests/e2e/playwright.config.ts
@@ -0,0 +1,84 @@
+import { defineConfig, devices } from '@playwright/test'
+import dotenv from 'dotenv'
+import path from 'path'
+
+dotenv.config({path: path.resolve(__dirname, '.env')})
+
+const isCI = !!process.env.CI;
+
+const enabledEditors = (process.env.HTML_EDITOR || '').split('|').filter(Boolean)
+
+const createWebServer = (editor: string, port: number) => ({
+ command: `yarn nx serve-static html-${editor}`,
+ port,
+ reuseExistingServer: true,
+ setTimeout: 30_000
+})
+
+// Map of editors to their ports, defined in their corresponding demo's webpack.config.js file
+const editorPortMap = {
+ 'ckeditor4': 8001,
+ 'ckeditor5': 8002,
+ 'froala': 8004,
+ 'tinymce5': 8006,
+ 'tinymce6': 8008,
+ 'tinymce7': 8009,
+ 'tinymce8': 8010,
+ 'generic': 8007,
+}
+
+// Creates web servers only for enabled editors in the HTML_EDITOR env variable
+const webServers = enabledEditors
+ .filter(editor => editorPortMap[editor as keyof typeof editorPortMap])
+ .map(editor => createWebServer(editor, editorPortMap[editor as keyof typeof editorPortMap]))
+
+export default defineConfig({
+ testDir: './tests',
+ fullyParallel: true,
+ forbidOnly: isCI,
+ retries: isCI ? 1 : 0,
+ workers: isCI ? '90%' : undefined,
+ reporter: [
+ ['html', { open: isCI ? 'never' : 'on-failure', outputFolder: 'playwright-report/html' }],
+ ['junit', { outputFile: 'test-results/results.xml' }],
+ isCI ? ['blob']: ['null'],
+ isCI ? ['github'] : ['list'],
+ ],
+ use: {
+ baseURL: process.env.USE_STAGING === 'true' ? 'https://integrations.wiris.kitchen' : '',
+ trace: isCI ? 'retain-on-failure' : 'on-first-retry',
+ screenshot: 'only-on-failure',
+ video: isCI ? 'off' : 'on-first-retry',
+ },
+ webServer: process.env.USE_STAGING === 'true' ? undefined : webServers,
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ viewport: { width: 1920, height: 1080 },
+ permissions: ['clipboard-read', 'clipboard-write']
+ }
+ },
+ {
+ name: 'firefox',
+ use: {
+ ...devices['Desktop Firefox'],
+ viewport: { width: 1920, height: 1080 }
+ }
+ },
+ {
+ name: 'webkit',
+ use: {
+ ...devices['Desktop Safari'],
+ viewport: { width: 1920, height: 1080 }
+ }
+ }
+ ],
+ outputDir: 'test-results',
+ timeout: 60_000,
+ expect: {
+ timeout: 10_000
+ },
+
+})
diff --git a/tests/e2e/tests/edit/edit_corner_cases.spec.ts b/tests/e2e/tests/edit/edit_corner_cases.spec.ts
new file mode 100644
index 000000000..9ef90344a
--- /dev/null
+++ b/tests/e2e/tests/edit/edit_corner_cases.spec.ts
@@ -0,0 +1,96 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+
+for (const editorName of editors) {
+ test.describe(`Edit equation (corner cases) - ${editorName} editor`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`MTHTML-81 Edit styled equation: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.styledSingleNumber.mathml)
+ await editor.waitForEquation(Equations.styledSingleNumber)
+
+ await editor.openWirisEditorForLastInsertedFormula(Toolbar.MATH, Equations.styledSingleNumber)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.typeEquationViaKeyboard('+1')
+ await wirisEditor.insertButton.click()
+ await wirisEditor.waitUntilClosed()
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.styledOnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+
+ test(`MTHTML-85 User edits a formula and continues typing text: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ await editor.openWirisEditorForLastInsertedFormula(Toolbar.MATH, Equations.singleNumber)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationViaKeyboard('+1')
+
+ const textToType = 'I can keep typing in the Editor'
+ await editor.pause(500)
+ await page.keyboard.type(textToType)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.OnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+
+ const isTextAfterEquation = await editor.isTextAfterEquation(textToType, Equations.OnePlusOne.altText)
+ expect(isTextAfterEquation).toBeTruthy()
+ })
+
+ test(`MTHTML-100 User edits a formula deleted during edition: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ await editor.openWirisEditorForLastInsertedFormula(Toolbar.MATH, Equations.singleNumber)
+ await wirisEditor.waitUntilLoaded()
+ await editor.clear() // Delete the formula in the background
+
+ // We continue with the insert in the wiris editor
+ await wirisEditor.insertEquationViaKeyboard('+1')
+
+ // No error should be visible, and the editor should be blank
+ expect(await editor.isEditorCleared()).toBeTruthy()
+
+ // Again, we do an insert + edit so we assure everything works as expected
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ await editor.openWirisEditorForLastInsertedFormula(Toolbar.MATH, Equations.singleNumber)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.pause(1000)
+ await wirisEditor.insertEquationViaKeyboard('+1')
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.OnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/edit/edit_hand.spec.ts b/tests/e2e/tests/edit/edit_hand.spec.ts
new file mode 100644
index 000000000..f88c6234e
--- /dev/null
+++ b/tests/e2e/tests/edit/edit_hand.spec.ts
@@ -0,0 +1,38 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import TypingMode from '../../enums/typing_mode'
+
+const editors = getEditorsFromEnv()
+const toolbars = Object.values(Toolbar)
+
+for (const editorName of editors) {
+ test.describe(`Edit equation by hand - ${editorName} editor`,{
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ for (const toolbar of toolbars) {
+ test(`@smoke MTHTML-8 Edit Hand equation with ${toolbar}: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(toolbar)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.typeEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await wirisEditor.pause(1000) // Wait for the equation to be processed
+ await wirisEditor.handModeButton.click()
+ await wirisEditor.pause(500)
+ await wirisEditor.insertButton.click()
+ await wirisEditor.waitUntilClosed()
+ await editor.waitForEquation(Equations.singleNumber)
+
+ await editor.openWirisEditorForLastInsertedFormula(toolbar, Equations.singleNumber)
+ await wirisEditor.waitUntilLoaded(TypingMode.HAND)
+
+ const typingMode = await wirisEditor.getMode()
+ expect(typingMode).toBe(TypingMode.HAND)
+ })
+ }
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/edit/edit_via_doble_click.spec.ts b/tests/e2e/tests/edit/edit_via_doble_click.spec.ts
new file mode 100644
index 000000000..e36b803d6
--- /dev/null
+++ b/tests/e2e/tests/edit/edit_via_doble_click.spec.ts
@@ -0,0 +1,56 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+
+for (const editorName of editors) {
+ test.describe(`Edit equation via double click - ${editorName} editor`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ // Skip test for Froala as double click is not available
+ test.skip(editorName === 'froala', `Double click not available in ${editorName}`)
+
+ test(`@smoke MTHTML-2 Edit Math equation: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ const equationInDOM = editor.getEquationElement(Equations.singleNumber)
+ await editor.clickElement(equationInDOM, 2)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationViaKeyboard('+1')
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.OnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+
+ test(`@smoke MTHTML-2 Edit Chemistry equation: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.CHEMISTRY)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ const equationInDOM = editor.getEquationElement(Equations.singleNumber)
+ await editor.clickElement(equationInDOM, 2)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationViaKeyboard('+1')
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.OnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/edit/edit_via_selection.spec.ts b/tests/e2e/tests/edit/edit_via_selection.spec.ts
new file mode 100644
index 000000000..2d8913cc2
--- /dev/null
+++ b/tests/e2e/tests/edit/edit_via_selection.spec.ts
@@ -0,0 +1,56 @@
+import { test, expect } from '@playwright/test'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import { getEditorsFromEnv } from '../../helpers/test-setup'
+import { setupEditor } from '../../helpers/test-setup'
+import Equation from '../../interfaces/equation'
+
+// Configure which editors to test via environment variables
+const editors = getEditorsFromEnv()
+const toolbars = Object.values(Toolbar)
+
+for (const editorName of editors) {
+ test.describe(`Edit equation via selection - ${editorName} editor`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ for (const toolbar of toolbars) {
+ test(`@smoke MTHTML-8 Edit Math equation with ${toolbar}: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ await editor.openWirisEditorForLastInsertedFormula(toolbar, Equations.singleNumber)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationViaKeyboard('+1')
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.OnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+
+ test(`@smoke MTHTML-8 Edit Chemistry equation with ${toolbar}: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.CHEMISTRY)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ await editor.openWirisEditorForLastInsertedFormula(toolbar, Equations.singleNumber)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationViaKeyboard('+1')
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.OnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+ }
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/editor/copy_cut_drop.spec.ts b/tests/e2e/tests/editor/copy_cut_drop.spec.ts
new file mode 100644
index 000000000..293e06cf9
--- /dev/null
+++ b/tests/e2e/tests/editor/copy_cut_drop.spec.ts
@@ -0,0 +1,78 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+
+for (const editorName of editors) { // TODO: review some flaky tests
+ test.describe(`Copy/Cut/Paste/Drag&Drop - ${editorName} editor`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`MTHTML-95 Copy-paste math formula with ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+ await page.keyboard.type('___') // Sending some text to force the copy/paste. Issue in Tiny
+
+ await editor.copyAllEditorContent()
+ await editor.clear()
+ await editor.paste()
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationCopied = equationsInHTMLEditor.every((equation: Equation) => equation.altText === Equations.singleNumber.altText) && (equationsInHTMLEditor.length === 1)
+ expect(isEquationCopied).toBeTruthy()
+ })
+
+ test(`MTHTML-96 Cut-paste math formula with ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+ await page.keyboard.type('___') // Sending some text to force the copy/paste. Issue in Tiny
+
+ await editor.cutAllEditorContent()
+ await editor.paste()
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationCut = equationsInHTMLEditor.every((equation: Equation) => equation.altText === Equations.singleNumber.altText) && (equationsInHTMLEditor.length === 1)
+ expect(isEquationCut).toBeTruthy()
+ })
+
+ test(`MTHTML-86 Drag-drop math formula with ${editorName} editor`, async ({ page }) => {
+ test.fixme((editorName === 'ckeditor5' || editorName === 'generic') && test.info().project.name === 'firefox', `Drag and drop not working for ${editorName} in Firefox`) // TODO: fix drag and drop in Firefox for ckeditor5 and generic editor
+
+ const unsupportedEditors = ['ckeditor4', 'tinymce5', 'tinymce6', 'tinymce7', 'tinymce8'] // WIP
+
+ // Skip test for unsupported editors
+ test.skip(unsupportedEditors.includes(editorName), `Drag and drop not supported for ${editorName}`)
+
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ const textToType = 'The equation will be relocated from after this text to before it'
+ await editor.appendText(textToType)
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.OnePlusOne.mathml)
+ await editor.waitForEquation(Equations.OnePlusOne)
+
+ await editor.dragDropLastFormula(Equations.OnePlusOne)
+ await editor.waitForEquation(Equations.OnePlusOne)
+
+ const isTextAfterEquation = await editor.isTextAfterEquation(textToType, Equations.OnePlusOne.altText)
+ expect(isTextAfterEquation).toBeTruthy()
+ })
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/editor/editor.spec.ts b/tests/e2e/tests/editor/editor.spec.ts
new file mode 100644
index 000000000..d8020c934
--- /dev/null
+++ b/tests/e2e/tests/editor/editor.spec.ts
@@ -0,0 +1,125 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+
+for (const editorName of editors) {
+ test.describe(`Editor functionality - ${editorName} editor`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test.describe('Undo and Redo', () => {
+ const isKnownIssue = editorName === 'generic';
+ test(`MTHTML-78 Undo and redo math formula with ${editorName} editor`, { tag: isKnownIssue ? ['@knownissue'] : [] } , async ({ page }) => {
+ test.fail(isKnownIssue, 'Known issue: generic editors fails to undo equation');
+
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ await editor.undo()
+ if ((await editor.getEquations()).length === 0) {
+ await editor.redo()
+ }
+ const isEquationRedone = ((await editor.getEquations()).length === 1)
+ expect(isEquationRedone).toBeTruthy()
+ })
+ })
+
+ test.describe('Resize', () => {
+ test(`MTHTML-22 Formulas cannot be resized ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.OnePlusOne.mathml)
+ await editor.waitForEquation(Equations.OnePlusOne)
+
+ const sizeBefore = await editor.getImageSize(Equations.OnePlusOne)
+ await editor.resizeImageEquation(Equations.OnePlusOne)
+ const sizeAfter = await editor.getImageSize(Equations.OnePlusOne)
+
+ const sameSize = sizeBefore?.height === sizeAfter?.height && sizeBefore?.width === sizeAfter?.width
+ expect(sameSize).toBeTruthy()
+ })
+ })
+
+ test.describe('Source code', () => {
+ test(`MTHTML-87 Edit source code of a math formula with ${editorName} editor`, async ({ page }) => {
+ const { editor } = await setupEditor(page, editorName)
+
+ // Skip test if editor doesn't support source code editing
+ const hasSourceCodeButton = editor.getSourceCodeEditorButton !== undefined
+ test.skip(!hasSourceCodeButton, `Source code editing not supported for ${editorName}`)
+
+ await editor.open()
+ await editor.clear()
+ //await editor.openWirisEditor(Toolbar.MATH) TODO: review if we want to open the wiris editor first, fails tests in some editors
+ await editor.clickSourceCodeEditor()
+ await editor.typeSourceText('')
+ await editor.clickSourceCodeEditor()
+ const sourceCodeEquation = await editor.getEquations()
+ expect(sourceCodeEquation[0].altText).toBe('1 plus 1')
+ })
+ })
+
+ test.describe('Styled text', () => {
+ test(`MTHTML-79 Validate formula insertion after typing text that contains styles: ${editorName} editor`, async ({ page }) => {
+ test.fixme((editorName === 'generic') && test.info().project.name === 'firefox', `Ctrl+B and Ctrl+I not working for generic editor on Firefox`)
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ const textBeginning = 'Text_Beginning'
+ const textEnd = 'Text_End' // The text with spaces brings some errors since the keys are sent without spaces
+
+ await editor.applyStyle() // TODO: not applying style in generic editor on firefox, ctrl+B opens bookmarks menu and ctrl+I opens page info
+ await page.keyboard.type(textBeginning)
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.OnePlusOne.mathml)
+ await editor.waitForEquation(Equations.OnePlusOne)
+
+ await page.keyboard.type(textEnd)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.OnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+
+ const isTextBeginningBoldAndItalic = await editor.isTextBoldAndItalic(textBeginning)
+ expect(isTextBeginningBoldAndItalic).toBeTruthy()
+
+ const isTextEndBoldAndItalic = await editor.isTextBoldAndItalic(textEnd)
+ expect(isTextEndBoldAndItalic).toBeTruthy()
+ })
+ })
+
+ test.describe('Text Alignment', () => {
+ test(`MTHTML-23 Validate formula alignment: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await page.keyboard.type('___')
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.OnePlusOne.mathml)
+ await editor.waitForEquation(Equations.OnePlusOne)
+ await page.keyboard.type('___')
+
+ await editor.checkElementAlignment()
+ // Note: Playwright doesn't have a direct equivalent to visual comparison
+ // TODO: This would need to be implemented using screenshot comparison libraries
+ })
+ })
+ })
+}
diff --git a/tests/e2e/tests/insert/insert.spec.ts b/tests/e2e/tests/insert/insert.spec.ts
new file mode 100644
index 000000000..ef2d25ca9
--- /dev/null
+++ b/tests/e2e/tests/insert/insert.spec.ts
@@ -0,0 +1,68 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+const toolbars = Object.values(Toolbar)
+
+for (const editorName of editors) {
+ for (const toolbar of toolbars) {
+ test.describe(`Insert equation - ${editorName} editor - ${toolbar}`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`@smoke MTHTML-1 Insert equation with ${toolbar} via keyboard: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(toolbar)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.insertEquationViaKeyboard('1')
+ await wirisEditor.pause(500) // Wait for the equation to be processed
+ await editor.waitForEquation(Equations.singleNumber)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.singleNumber.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+
+ test(`Insert equation with ${toolbar} using MathML: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(toolbar)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.singleNumber.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+ })
+ }
+
+ test.describe(`ALT Attribute - ${editorName} editor`, () => {
+ test(`MTHTML-19 Formula - ALT attribute: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.insertEquationViaKeyboard('1')
+ await wirisEditor.pause(500) // Wait for the equation to be processed
+ await editor.waitForEquation(Equations.singleNumber)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.singleNumber.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/insert/insert_corner_cases.spec.ts b/tests/e2e/tests/insert/insert_corner_cases.spec.ts
new file mode 100644
index 000000000..47e15595e
--- /dev/null
+++ b/tests/e2e/tests/insert/insert_corner_cases.spec.ts
@@ -0,0 +1,66 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+const toolbars = Object.values(Toolbar)
+
+for (const editorName of editors) {
+ for (const toolbar of toolbars) {
+ test.describe(`Insert equation (corner cases) - ${editorName} editor - ${toolbar}`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`MTHTML-80 Insert styled equation: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(toolbar)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.insertEquationUsingEntryForm(Equations.styledOnePlusOne.mathml)
+ await editor.waitForEquation(Equations.styledOnePlusOne)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.styledOnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+
+ test(`MTHTML-68 Insert equation with special characters: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(toolbar)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.insertEquationUsingEntryForm(Equations.specialCharacters.mathml)
+ await editor.waitForEquation(Equations.specialCharacters)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.specialCharacters.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+
+ test(`MTHTML-90 User inserts a formula when the editor input doesn't have focus: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ // Insert a formula using only the keyboard (click MT button > type > tab to insert > enter) and keep writing
+ await editor.open()
+ await editor.openWirisEditor(toolbar)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.typeEquationViaKeyboard('1+1')
+ await page.keyboard.press('Tab')
+ await wirisEditor.pause(500)
+ await page.keyboard.press('Enter')
+
+ await editor.waitForEquation(Equations.OnePlusOne)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.OnePlusOne.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+ })
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/insert/insert_hand.spec.ts b/tests/e2e/tests/insert/insert_hand.spec.ts
new file mode 100644
index 000000000..89a1f5779
--- /dev/null
+++ b/tests/e2e/tests/insert/insert_hand.spec.ts
@@ -0,0 +1,37 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import EquationEntryMode from '../../enums/equation_entry_mode'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+const toolbars = Object.values(Toolbar)
+
+for (const editorName of editors) {
+ for (const toolbar of toolbars) {
+ test.describe(`Insert equation via Hand - ${editorName} editor - ${toolbar}`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`@smoke MTHTML-20 Insert a handwritten equation using ${toolbar}: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(toolbar)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.showEquationEntryForm(EquationEntryMode.MATHML)
+ await wirisEditor.typeEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await wirisEditor.pause(500) // Wait for the equation to be processed
+ await wirisEditor.handModeButton.click()
+ await wirisEditor.pause(500)
+ await wirisEditor.insertButton.click()
+ await wirisEditor.waitUntilClosed()
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.singleNumber.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+ })
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/latex/latex.spec.ts b/tests/e2e/tests/latex/latex.spec.ts
new file mode 100644
index 000000000..7b9140b78
--- /dev/null
+++ b/tests/e2e/tests/latex/latex.spec.ts
@@ -0,0 +1,87 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Equations from '../../enums/equations'
+import Toolbar from '../../enums/toolbar'
+import EquationEntryMode from '../../enums/equation_entry_mode'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+
+for (const editorName of editors) {
+ test.describe(`LaTeX - ${editorName} editor`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`Validate LaTeX formula detection: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.appendText('$$ ' + Equations.squareRootY.latex + '$$')
+
+ await editor.selectItemAtCursor()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertButton.click()
+ await editor.waitForLatexExpression('\\sqrt y')
+
+ const latexEquations = await editor.getLatexEquationsInEditField()
+ expect(latexEquations).toContain('\\sqrt y')
+ })
+
+ test(`Insert formula using LaTeX equation entry form: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.showEquationEntryForm(EquationEntryMode.LATEX)
+ await wirisEditor.insertEquationUsingEntryForm(Equations.squareRootY.latex ?? (() => { throw new Error('LaTeX equation is undefined') })())
+ await wirisEditor.pause(500) // Wait for the equation to be processed
+ await editor.waitForEquation(Equations.squareRootY)
+
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.squareRootY.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+
+ test(`@smoke MTHTML-10 Edit LaTeX equation: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.clear()
+ await editor.appendText('$$ ' + Equations.squareRootY.latex + '$$')
+ await editor.selectItemAtCursor()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.appendText('+5')
+ await wirisEditor.pause(500)
+ await wirisEditor.insertButton.click()
+ await wirisEditor.waitUntilClosed()
+
+ await editor.waitForLatexExpression(Equations.squareRootYPlusFive.latex ?? (() => { throw new Error('LaTeX equation is undefined') })())
+ const latexEquations = await editor.getLatexEquationsInEditField()
+ expect(latexEquations).toContain(Equations.squareRootYPlusFive.latex ?? (() => { throw new Error('LaTeX equation is undefined') })())
+ })
+
+ test(`Edit empty LaTeX equation: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+ const INPUT_FORMULA = 'sin x'
+
+ await editor.open()
+ await editor.clear()
+ await editor.appendText('$$$$')
+ await editor.selectItemAtCursor()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.insertEquationViaKeyboard(INPUT_FORMULA)
+
+ await editor.waitForLatexExpression('\\sin\\;x')
+ const latexEquations = await editor.getLatexEquationsInEditField()
+ expect(latexEquations).toContain('\\sin\\;x')
+ })
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/modal/confirmation_dialog.spec.ts b/tests/e2e/tests/modal/confirmation_dialog.spec.ts
new file mode 100644
index 000000000..b2fff3823
--- /dev/null
+++ b/tests/e2e/tests/modal/confirmation_dialog.spec.ts
@@ -0,0 +1,108 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Toolbar from '../../enums/toolbar'
+import Equations from '../../enums/equations'
+import Equation from '../../interfaces/equation'
+
+const editors = getEditorsFromEnv()
+
+for (const editorName of editors) {
+ test.describe(`Confirmation dialog - ${editorName} editor`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`MTHTML-11 When wiris editor contains no changes and user clicks the Cancel button, modal closes: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.cancelButton.click()
+
+ await expect(wirisEditor.wirisEditorWindow).not.toBeVisible()
+ })
+
+ test(`MTHTML-3 When wiris editor contains changes and user clicks the Cancel button, modal displays confirmation dialog: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+ const EQUATION_TO_TYPE_VIA_KEYBOARD = '111'
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.typeEquationViaKeyboard(EQUATION_TO_TYPE_VIA_KEYBOARD)
+
+ await wirisEditor.cancelButton.click() // confirmation dialog should be displayed after this click!
+
+ await expect(wirisEditor.wirisEditorWindow).toBeVisible()
+ await expect(wirisEditor.confirmationDialog).toBeVisible()
+ })
+
+ test(`When confirmation dialog is displayed and user clicks cancel, confirmation dialog closes: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+ const EQUATION_TO_TYPE_VIA_KEYBOARD = '221221'
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.typeEquationViaKeyboard(EQUATION_TO_TYPE_VIA_KEYBOARD)
+ await wirisEditor.cancelButton.click() // confirmation dialog should be displayed after this click!
+
+ await wirisEditor.confirmationDialogCancelButton.click()
+
+ await expect(wirisEditor.confirmationDialog).not.toBeVisible()
+ await expect(wirisEditor.wirisEditorWindow).toBeVisible()
+ })
+
+ test(`MTHTML-12 When confirmation dialog is displayed and user clicks the Close button, wiris editor closes: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+ const EQUATION_TO_TYPE_VIA_KEYBOARD = '3'
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.typeEquationViaKeyboard(EQUATION_TO_TYPE_VIA_KEYBOARD)
+ await wirisEditor.closeButton.click() // confirmation dialog should be displayed after this click!
+
+ await wirisEditor.confirmationDialogCloseButton.click()
+
+ await expect(wirisEditor.confirmationDialog).not.toBeVisible()
+ await expect(wirisEditor.wirisEditorWindow).not.toBeVisible()
+ })
+
+ test(`MTHTML-14 When confirmation dialog is displayed and user press the ESC key, wiris editor closes: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+ const EQUATION_TO_TYPE_VIA_KEYBOARD = '3'
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.typeEquationViaKeyboard(EQUATION_TO_TYPE_VIA_KEYBOARD)
+ await page.keyboard.press('Escape') // confirmation dialog should be displayed after this!
+
+ await wirisEditor.confirmationDialogCloseButton.click()
+
+ await expect(wirisEditor.confirmationDialog).not.toBeVisible()
+ await expect(wirisEditor.wirisEditorWindow).not.toBeVisible()
+ })
+
+ test(`MTHTML-29 Insert an equation after aborting the cancel: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+ const EQUATION_TO_INSERT_VIA_KEYBOARD = '1'
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.typeEquationViaKeyboard(EQUATION_TO_INSERT_VIA_KEYBOARD)
+
+ await wirisEditor.cancelButton.click() // confirmation dialog should be displayed after this click!
+ await wirisEditor.confirmationDialogCancelButton.click()
+ await wirisEditor.insertButton.click()
+ await wirisEditor.waitUntilClosed()
+
+ await editor.waitForEquation(Equations.singleNumber)
+ const equationsInHTMLEditor = await editor.getEquations()
+ const isEquationPresent = equationsInHTMLEditor.some((equation: Equation) => equation.altText === Equations.singleNumber.altText)
+ expect(isEquationPresent).toBeTruthy()
+ })
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/modal/toolbar.spec.ts b/tests/e2e/tests/modal/toolbar.spec.ts
new file mode 100644
index 000000000..82db1d164
--- /dev/null
+++ b/tests/e2e/tests/modal/toolbar.spec.ts
@@ -0,0 +1,102 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import Toolbar from '../../enums/toolbar'
+
+const editors = getEditorsFromEnv()
+
+for (const editorName of editors) {
+ test.describe(`Toolbar functions - ${editorName} editor`, {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`MTHTML-13 When the modal is displayed and close button is clicked, wiris modal closes: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.closeButton.click()
+ await wirisEditor.waitUntilClosed()
+
+ await expect(wirisEditor.wirisEditorWindow).not.toBeVisible()
+ })
+
+ test(`When the modal is displayed and enable full screen button is clicked, display changes to full-screen: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.fullScreenButton.click()
+
+ await expect(wirisEditor.modalOverlay).toBeVisible()
+ })
+
+ test(`When the modal is displayed in full screen mode and disable full-screen button is clicked, display changes to normal: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.fullScreenButton.click()
+
+ await wirisEditor.exitFullScreenButton.click()
+
+ await expect(wirisEditor.modalOverlay).not.toBeVisible()
+ })
+
+ // WIP
+ test.fixme(`When the modal is displayed and minimize button is clicked, modal is minimized: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await wirisEditor.minimizeButton.click()
+ await editor.pause(1000)
+
+ await expect(wirisEditor.wirisEditorWindow).not.toBeVisible()
+ })
+
+ // WIP
+ test.fixme(`When the modal is minimized and user double clicks the banner, modal opens: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.minimizeButton.click()
+ await wirisEditor.waitUntilClosed()
+
+ await wirisEditor.modalTitle.click()
+
+ await expect(wirisEditor.wirisEditorWindow).toBeVisible()
+ })
+
+ test.fixme(`When the modal is minimized from full-screen mode and user double clicks the banner, modal opens in full-screen mode: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.fullScreenButton.click()
+
+ await wirisEditor.minimizeButton.click()
+ // TODO: This test seems incomplete in the original WebDriverIO version
+ })
+
+ test(`MTHTML-15 When the modal is displayed and ESC key is pressed, wiris modal closes: ${editorName} editor`, async ({ page }) => {
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+
+ await page.keyboard.press('Escape')
+ await wirisEditor.waitUntilClosed()
+
+ await expect(wirisEditor.wirisEditorWindow).not.toBeVisible()
+ })
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tests/telemetry/telemetry.spec.ts b/tests/e2e/tests/telemetry/telemetry.spec.ts
new file mode 100644
index 000000000..ca9abf388
--- /dev/null
+++ b/tests/e2e/tests/telemetry/telemetry.spec.ts
@@ -0,0 +1,45 @@
+import { test, expect } from '@playwright/test'
+import { setupEditor, getEditorsFromEnv } from '../../helpers/test-setup'
+import { captureTelemetryRequests } from '../../helpers/network'
+import Toolbar from '../../enums/toolbar'
+import Equations from '../../enums/equations'
+
+const editors = getEditorsFromEnv()
+
+for (const editorName of editors) {
+ test.describe('Telemetry', {
+ tag: [`@${editorName}`, '@regression'],
+ }, () => {
+ test(`MTHTML-59 MathType all events testing: ${editorName} editor`, async ({ page, browserName }) => {
+ // Skip Firefox as mentioned in original test
+ test.skip(browserName === 'firefox', 'Telemetry tests are skipped on Firefox')
+
+ const { editor, wirisEditor } = await setupEditor(page, editorName)
+
+ // Enable Network Listener and capture all telemetry requests
+ const foundEvents: string[] = []
+ await captureTelemetryRequests(page, foundEvents)
+
+ // Perform the actions that will trigger the telemetry events
+ await editor.open()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.cancelButton.click()
+ await editor.clear()
+ await editor.openWirisEditor(Toolbar.MATH)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.insertEquationUsingEntryForm(Equations.singleNumber.mathml)
+ await editor.waitForEquation(Equations.singleNumber)
+ await editor.openWirisEditorForLastInsertedFormula(Toolbar.MATH, Equations.singleNumber)
+ await wirisEditor.waitUntilLoaded()
+ await wirisEditor.typeEquationViaKeyboard('+1')
+ await wirisEditor.insertButton.click()
+ await wirisEditor.waitUntilClosed()
+ await editor.pause(1500) // This pause is needed to wait for the last event
+
+ // Check that all events have been sent are the ones we expect, in the same order
+ const expectEvents = ['STARTED_TELEMETRY_SESSION', 'CLOSED_MTCT_EDITOR', 'OPENED_MTCT_EDITOR', 'INSERTED_FORMULA', 'CLOSED_MTCT_EDITOR', 'OPENED_MTCT_EDITOR', 'INSERTED_FORMULA', 'CLOSED_MTCT_EDITOR']
+ expect(JSON.stringify(expectEvents) === JSON.stringify(foundEvents)).toBeTruthy()
+ })
+ })
+}
\ No newline at end of file
diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json
new file mode 100644
index 000000000..37aa9dd70
--- /dev/null
+++ b/tests/e2e/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022"],
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": false,
+ "outDir": "./dist",
+ "rootDir": ".",
+ "types": ["node", "@playwright/test"]
+ },
+ "include": [
+ "tests/**/*",
+ "page-objects/**/*",
+ "helpers/**/*",
+ "enums/**/*",
+ "interfaces/**/*",
+ "playwright.config.ts"
+ ],
+ "exclude": ["node_modules", "dist", "test-results"]
+}
\ No newline at end of file