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. - -![Minimal MathType integration snapshot](mathtype-web-app.png) - -### MathType Modal Window - -The MathType Modal Window consists on the following elements: - -![Diagram of the MathType modal window](modal.jpg) - -> 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": "x=-b±b2-4ac2a", - "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: '1' + }, + styledSingleNumber: { + altText: 'bold italic 1', + mathml: '1' + }, + squareRootY: { + altText: 'square root of y', + mathml: 'y', + latex: '\\sqrt{y}' + }, + squareRootYPlusFive: { + altText: 'square root of y plus 5', + mathml: 'y5', + latex: '\\sqrt y+5' + }, + OnePlusOne: { + altText: '1 plus 1', + mathml: '1+1' + }, + styledOnePlusOne: { + altText: 'bold italic 1 bold italic plus bold italic 1', + mathml: '1+1' + }, + 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('1+1') + 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