diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 37c8ede08..cb89d2b91 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -49,12 +49,12 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Only install direct dependencies run: pnpm config set auto-install-peers false - - - name: Install etherpad plugins - # We intentionally install an old ep_align version to test upgrades to - # the minor version number. The --legacy-peer-deps flag is required to - # work around a bug in npm v7: https://github.com/npm/cli/issues/2199 - run: pnpm install --workspace-root ep_align@0.2.27 + #- + # name: Install etherpad plugins + # # We intentionally install an old ep_align version to test upgrades to + # # the minor version number. The --legacy-peer-deps flag is required to + # # work around a bug in npm v7: https://github.com/npm/cli/issues/2199 + # run: pnpm install --workspace-root ep_align@0.2.27 # Etherpad core dependencies must be installed after installing the # plugin's dependencies, otherwise npm will try to hoist common # dependencies by removing them from src/node_modules and installing them @@ -67,9 +67,9 @@ jobs: - name: Install all dependencies and symlink for ep_etherpad-lite run: bin/installDeps.sh - - - name: Install etherpad plugins - run: rm -Rf node_modules/ep_align/static/tests/* + #- + # name: Install etherpad plugins + # run: rm -Rf node_modules/ep_align/static/tests/* - name: export GIT_HASH to env id: environment @@ -79,7 +79,7 @@ jobs: run: cp settings.json.template settings.json - name: Write custom settings.json that enables the Admin UI tests - run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json" + run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme1\",\"is_admin\":true}}/' settings.json" - name: increase maxHttpBufferSize run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 10000000/' settings.json" @@ -87,23 +87,59 @@ jobs: name: Disable import/export rate limiting run: | sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json - - - name: Remove standard frontend test files, so only admin tests are run - run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs - - - uses: saucelabs/sauce-connect-action@v2.3.6 - with: - username: ${{ secrets.SAUCE_USERNAME }} - accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} - tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }} - - - name: Run the frontend admin tests - shell: bash - env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} - TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} - GIT_HASH: ${{ steps.environment.outputs.sha_short }} + - name: Build admin frontend + working-directory: admin run: | - src/tests/frontend/travis/adminrunner.sh + pnpm install + pnpm run build + # name: Run the frontend admin tests + # shell: bash + # env: + # SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + # SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + # SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} + # TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} + # GIT_HASH: ${{ steps.environment.outputs.sha_short }} + # run: | + # src/tests/frontend/travis/adminrunner.sh + #- + # uses: saucelabs/sauce-connect-action@v2.3.6 + # with: + # username: ${{ secrets.SAUCE_USERNAME }} + # accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} + # tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }} + #- + # name: Run the frontend admin tests + # shell: bash + # env: + # SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + # SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + # SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} + # TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} + # GIT_HASH: ${{ steps.environment.outputs.sha_short }} + # run: | + # src/tests/frontend/travis/adminrunner.sh + - name: Run the frontend admin tests + shell: bash + run: | + pnpm run dev & + connected=false + can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true + } + now() { date +%s; } + start=$(now) + while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 + done + cd src + pnpm exec playwright install + pnpm exec playwright install-deps + pnpm run test-admin + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.node }} + path: src/playwright-report/ + retention-days: 30 diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index a5498e1e8..a5220f73c 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -75,6 +75,81 @@ jobs: GIT_HASH: ${{ steps.environment.outputs.sha_short }} run: | src/tests/frontend/travis/runner.sh + withoutpluginsPlaywright: + name: without plugins + runs-on: ubuntu-latest + if: ${{ github.actor != 'dependabot[bot]' }} + + steps: + - + name: Generate Sauce Labs strings + id: sauce_strings + run: | + printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' + printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' + - + name: Checkout repository + uses: actions/checkout@v4 + - + uses: actions/setup-node@v4 + with: + node-version: 21 + - uses: pnpm/action-setup@v3 + name: Install pnpm + with: + version: 8 + run_install: false + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Only install direct dependencies + run: pnpm config set auto-install-peers false + - + name: Install all dependencies and symlink for ep_etherpad-lite + run: bin/installDeps.sh + - + name: export GIT_HASH to env + id: environment + run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + - + name: Create settings.json + run: cp settings.json.template settings.json + - + name: Disable import/export rate limiting + run: | + sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json + - name: Run the frontend tests + shell: bash + run: | + pnpm run dev & + connected=false + can_connect() { + curl -sSfo /dev/null http://localhost:9001/ || return 1 + connected=true + } + now() { date +%s; } + start=$(now) + while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do + sleep 1 + done + cd src + pnpm exec playwright install + pnpm exec playwright install-deps + pnpm run test-ui + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.node }} + path: src/playwright-report/ + retention-days: 30 withplugins: name: with plugins @@ -175,3 +250,5 @@ jobs: GIT_HASH: ${{ steps.environment.outputs.sha_short }} run: | src/tests/frontend/travis/runner.sh + + diff --git a/.gitignore b/.gitignore index 2a8335497..7d65188e7 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ out/ plugin_packages pnpm-lock.yaml /src/templates/admin +/src/test-results +playwright-report +state.json diff --git a/admin/src/pages/HomePage.tsx b/admin/src/pages/HomePage.tsx index fd29e390e..c43585d0a 100644 --- a/admin/src/pages/HomePage.tsx +++ b/admin/src/pages/HomePage.tsx @@ -1,5 +1,5 @@ import {useStore} from "../store/store.ts"; -import {useEffect, useState} from "react"; +import {useEffect, useMemo, useState} from "react"; import {InstalledPlugin, PluginDef, SearchParams} from "./Plugin.ts"; import {useDebounce} from "../utils/useDebounce.ts"; import {Trans, useTranslation} from "react-i18next"; @@ -9,6 +9,18 @@ export const HomePage = () => { const pluginsSocket = useStore(state=>state.pluginsSocket) const [plugins,setPlugins] = useState([]) const [installedPlugins, setInstalledPlugins] = useState([]) + const sortedInstalledPlugins = useMemo(()=>{ + return installedPlugins.sort((a, b)=>{ + if(a.name < b.name){ + return -1 + } + if(a.name > b.name){ + return 1 + } + return 0 + }) + + } ,[installedPlugins]) const [searchParams, setSearchParams] = useState({ offset: 0, limit: 99999, @@ -125,7 +137,7 @@ export const HomePage = () => { - {installedPlugins.map((plugin, index) => { + {sortedInstalledPlugins.map((plugin, index) => { return {plugin.name} {plugin.version} diff --git a/admin/src/pages/LoginScreen.tsx b/admin/src/pages/LoginScreen.tsx index 7860368f8..b3d8e64bb 100644 --- a/admin/src/pages/LoginScreen.tsx +++ b/admin/src/pages/LoginScreen.tsx @@ -33,9 +33,9 @@ export const LoginScreen = ()=>{

Login Etherpad

Username
- setUsername(v.target.value)} placeholder="Username"/> + setUsername(v.target.value)} placeholder="Username"/>
Passwort
- setPassword(v.target.value)} placeholder="Password"/>
diff --git a/admin/src/utils/LoadingScreen.tsx b/admin/src/utils/LoadingScreen.tsx index a234dfc38..b3ea51e13 100644 --- a/admin/src/utils/LoadingScreen.tsx +++ b/admin/src/utils/LoadingScreen.tsx @@ -5,7 +5,7 @@ export const LoadingScreen = ()=>{ const showLoading = useStore(state => state.showLoading) return - +
diff --git a/package.json b/package.json index 891edbd5a..a4aac4bf3 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,11 @@ "dev": "pnpm --filter ep_etherpad-lite run dev", "prod": "pnpm --filter ep_etherpad-lite run prod", "ts-check": "pnpm --filter ep_etherpad-lite run ts-check", - "ts-check:watch": "pnpm --filter ep_etherpad-lite run ts-check:watch" + "ts-check:watch": "pnpm --filter ep_etherpad-lite run ts-check:watch", + "test-ui": "pnpm --filter ep_etherpad-lite run test-ui", + "test-ui:ui": "pnpm --filter ep_etherpad-lite run test-ui:ui", + "test-admin": "pnpm --filter ep_etherpad-lite run test-admin", + "test-admin:ui": "pnpm --filter ep_etherpad-lite run test-admin:ui" }, "dependencies": { "ep_etherpad-lite": "workspace:./src" @@ -35,4 +39,4 @@ }, "version": "1.9.7", "license": "Apache-2.0" -} \ No newline at end of file +} diff --git a/src/node/hooks/express/admin.ts b/src/node/hooks/express/admin.ts index 3c85d072a..fca17c99a 100644 --- a/src/node/hooks/express/admin.ts +++ b/src/node/hooks/express/admin.ts @@ -1,6 +1,7 @@ 'use strict'; import {ArgsExpressType} from "../../types/ArgsExpressType"; import path from "path"; +import fs from "fs"; const settings = require('ep_etherpad-lite/node/utils/Settings'); const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin'); @@ -16,7 +17,13 @@ exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Functi args.app.get('/admin/*', (req:any, res:any, next:Function) => { if (req.path.includes('.')) { const relativPath = req.path.split('/admin/')[1]; - res.sendFile(path.join(ADMIN_PATH, relativPath)); + try { + if (fs.statSync(path.join(ADMIN_PATH, relativPath)).isFile()) { + res.sendFile(path.join(ADMIN_PATH, relativPath)); + } + } catch (err) { + res.status(404).send('404: Not Found'); + } } else { res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); res.header('Expires', '-1'); diff --git a/src/package.json b/src/package.json index d680d767a..213c89039 100644 --- a/src/package.json +++ b/src/package.json @@ -95,6 +95,7 @@ "mocha-froth": "^0.2.10", "nodeify": "^1.0.1", "openapi-schema-validation": "^0.4.2", + "@playwright/test": "^1.42.1", "selenium-webdriver": "^4.18.1", "set-cookie-parser": "^2.6.0", "sinon": "^17.0.1", @@ -118,7 +119,11 @@ "dev": "node --import tsx node/server.ts", "prod": "node --import tsx node/server.ts", "ts-check": "tsc --noEmit", - "ts-check:watch": "tsc --noEmit --watch" + "ts-check:watch": "tsc --noEmit --watch", + "test-ui": "npx playwright test tests/frontend-new/specs", + "test-ui:ui": "npx playwright test tests/frontend-new/specs --ui", + "test-admin": "npx playwright test tests/frontend-new/admin-spec --workers 1", + "test-admin:ui": "npx playwright test tests/frontend-new/admin-spec --ui --workers 1" }, "version": "1.9.7", "license": "Apache-2.0" diff --git a/src/playwright.config.ts b/src/playwright.config.ts new file mode 100644 index 000000000..f5a103ce6 --- /dev/null +++ b/src/playwright.config.ts @@ -0,0 +1,69 @@ +import {defineConfig, devices, test} from '@playwright/test'; + + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/frontend-new/', + timeout: 90000, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + retries: 3, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + baseURL: "localhost:9001", + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/src/tests/frontend-new/admin-spec/adminsettings.spec.ts b/src/tests/frontend-new/admin-spec/adminsettings.spec.ts new file mode 100644 index 000000000..8794eef90 --- /dev/null +++ b/src/tests/frontend-new/admin-spec/adminsettings.spec.ts @@ -0,0 +1,59 @@ +import {expect, test} from "@playwright/test"; +import {loginToAdmin, restartEtherpad, saveSettings} from "../helper/adminhelper"; + +test.beforeEach(async ({ page })=>{ + await loginToAdmin(page, 'admin', 'changeme1'); +}) + +test.describe('admin settings',()=> { + + + test('Are Settings visible, populated, does save work', async ({page}) => { + await page.goto('http://localhost:9001/admin/settings'); + await page.waitForSelector('.settings'); + const settings = page.locator('.settings'); + await expect(settings).not.toBeEmpty(); + + const settingsVal = await settings.inputValue() + const settingsLength = settingsVal.length + + await settings.fill(`/* test */\n${settingsVal}`) + const newValue = await settings.inputValue() + expect(newValue).toContain('/* test */') + expect(newValue.length).toEqual(settingsLength+11) + await saveSettings(page) + + // Check if the changes were actually saved + await page.reload() + await page.waitForSelector('.settings'); + await expect(settings).not.toBeEmpty(); + + const newSettings = page.locator('.settings'); + + const newSettingsVal = await newSettings.inputValue() + expect(newSettingsVal).toContain('/* test */') + + + // Change back to old settings + await newSettings.fill(settingsVal) + await saveSettings(page) + + await page.reload() + await page.waitForSelector('.settings'); + await expect(settings).not.toBeEmpty(); + const oldSettings = page.locator('.settings'); + const oldSettingsVal = await oldSettings.inputValue() + expect(oldSettingsVal).toEqual(settingsVal) + expect(oldSettingsVal.length).toEqual(settingsLength) + }) + + test('restart works', async function ({page}) { + await page.goto('http://localhost:9001/admin/settings'); + await page.waitForSelector('.settings') + await restartEtherpad(page) + await page.waitForSelector('.settings') + const settings = page.locator('.settings'); + await expect(settings).not.toBeEmpty(); + await page.waitForSelector('.menu') + }); +}) diff --git a/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts b/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts new file mode 100644 index 000000000..9dc7c7a20 --- /dev/null +++ b/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts @@ -0,0 +1,39 @@ +import {expect, test} from "@playwright/test"; +import {loginToAdmin} from "../helper/adminhelper"; + +test.beforeEach(async ({ page })=>{ + await loginToAdmin(page, 'admin', 'changeme1'); + await page.goto('http://localhost:9001/admin/help') +}) + +test('Shows troubleshooting page manager', async ({page}) => { + await page.goto('http://localhost:9001/admin/help') + await page.waitForSelector('.menu') + const menu = page.locator('.menu'); + await expect(menu.locator('li')).toHaveCount(4); +}) + +test('Shows a version number', async function ({page}) { + await page.goto('http://localhost:9001/admin/help') + await page.waitForSelector('.menu') + const helper = page.locator('.help-block').locator('div').nth(1) + const version = (await helper.textContent())!.split('.'); + expect(version.length).toBe(3) +}); + +test('Lists installed parts', async function ({page}) { + await page.goto('http://localhost:9001/admin/help') + await page.waitForSelector('.menu') + await page.waitForSelector('.innerwrapper ul') + const parts = page.locator('.innerwrapper ul').nth(1); + expect(await parts.textContent()).toContain('ep_etherpad-lite/adminsettings'); +}); + +test('Lists installed hooks', async function ({page}) { + await page.goto('http://localhost:9001/admin/help') + await page.waitForSelector('.menu') + await page.waitForSelector('.innerwrapper ul') + const helper = page.locator('.innerwrapper ul').nth(2); + expect(await helper.textContent()).toContain('express'); +}); + diff --git a/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts b/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts new file mode 100644 index 000000000..591448636 --- /dev/null +++ b/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts @@ -0,0 +1,111 @@ +import {expect, test} from "@playwright/test"; +import {loginToAdmin} from "../helper/adminhelper"; + +test.beforeEach(async ({ page })=>{ + await loginToAdmin(page, 'admin', 'changeme1'); + await page.goto('http://localhost:9001/admin/plugins') +}) + + +test.describe('Plugins page', ()=> { + + test('List some plugins', async ({page}) => { + await page.waitForSelector('.search-field'); + const pluginTable = page.locator('table tbody').nth(1); + await expect(pluginTable).not.toBeEmpty() + const plugins = await pluginTable.locator('tr').count() + expect(plugins).toBeGreaterThan(10) + }) + + test('Searches for a plugin', async ({page}) => { + await page.waitForSelector('.search-field'); + await page.click('.search-field') + await page.keyboard.type('ep_font_color3') + await page.keyboard.press('Enter') + const pluginTable = page.locator('table tbody').nth(1); + await expect(pluginTable.locator('tr')).toHaveCount(1) + await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color3') + }) + + + test('Attempt to Install and Uninstall a plugin', async ({page}) => { + await page.waitForSelector('.search-field'); + const pluginTable = page.locator('table tbody').nth(1); + await expect(pluginTable).not.toBeEmpty({ + timeout: 15000 + }) + const plugins = await pluginTable.locator('tr').count() + expect(plugins).toBeGreaterThan(10) + + // Now everything is loaded, lets install a plugin + + await page.click('.search-field') + await page.keyboard.type('ep_font_color3') + await page.keyboard.press('Enter') + + await expect(pluginTable.locator('tr')).toHaveCount(1) + const pluginRow = pluginTable.locator('tr').first() + await expect(pluginRow).toContainText('ep_font_color3') + + // Select Installation button + await pluginRow.locator('td').nth(4).locator('button').first().click() + await page.waitForTimeout(100) + await page.waitForSelector('table tbody') + const installedPlugins = page.locator('table tbody').first() + const installedPluginsRows = installedPlugins.locator('tr') + await expect(installedPluginsRows).toHaveCount(2, { + timeout: 15000 + }) + + const installedPluginRow = installedPluginsRows.nth(1) + + await expect(installedPluginRow).toContainText('ep_font_color3') + await installedPluginRow.locator('td').nth(2).locator('button').first().click() + + // Wait for the uninstallation to complete + await expect(installedPluginsRows).toHaveCount(1, { + timeout: 15000 + }) + }) +}) + + +/* + it('Attempt to Update a plugin', async function () { + this.timeout(280000); + + await helper.waitForPromise(() => helper.admin$('.results').children().length > 50, 20000); + + if (helper.admin$('.ep_align').length === 0) this.skip(); + + await helper.waitForPromise( + () => helper.admin$('.ep_align .version').text().split('.').length >= 2); + + const minorVersionBefore = + parseInt(helper.admin$('.ep_align .version').text().split('.')[1]); + + if (!minorVersionBefore) { + throw new Error('Unable to get minor number of plugin, is the plugin installed?'); + } + + if (minorVersionBefore !== 2) this.skip(); + + helper.waitForPromise( + () => helper.admin$('.ep_align .do-update').length === 1); + + await timeout(500); // HACK! Please submit better fix.. + const $doUpdateButton = helper.admin$('.ep_align .do-update'); + $doUpdateButton.trigger('click'); + + // ensure its showing as Updating + await helper.waitForPromise( + () => helper.admin$('.ep_align .message').text() === 'Updating'); + + // Ensure it's a higher minor version IE 0.3.x as 0.2.x was installed + // Coverage for https://github.com/ether/etherpad-lite/issues/4536 + await helper.waitForPromise(() => parseInt(helper.admin$('.ep_align .version') + .text() + .split('.')[1]) > minorVersionBefore, 60000, 1000); + // allow 50 seconds, check every 1 second. + }); + */ diff --git a/src/tests/frontend-new/helper/adminhelper.ts b/src/tests/frontend-new/helper/adminhelper.ts new file mode 100644 index 000000000..d6b5d5615 --- /dev/null +++ b/src/tests/frontend-new/helper/adminhelper.ts @@ -0,0 +1,32 @@ +import {expect, Page} from "@playwright/test"; + +export const loginToAdmin = async (page: Page, username: string, password: string) => { + + await page.goto('http://localhost:9001/admin/'); + + await page.waitForSelector('input[name="username"]'); + await page.fill('input[name="username"]', username); + await page.fill('input[name="password"]', password); + await page.click('input[type="button"]'); +} + + +export const saveSettings = async (page: Page) => { + // Click save + await page.locator('.settings-button-bar').locator('button').first().click() + await page.waitForSelector('.ToastRootSuccess') +} + +export const restartEtherpad = async (page: Page) => { + // Click restart + const restartButton = page.locator('.settings-button-bar').locator('.settingsButton').nth(1) + const settings = page.locator('.settings'); + await expect(settings).not.toBeEmpty(); + await expect(restartButton).toBeVisible() + await page.locator('.settings-button-bar') + .locator('.settingsButton') + .nth(1) + .click() + await page.waitForTimeout(500) + await page.waitForSelector('.settings') +} diff --git a/src/tests/frontend-new/helper/padHelper.ts b/src/tests/frontend-new/helper/padHelper.ts new file mode 100644 index 000000000..b8e4244ec --- /dev/null +++ b/src/tests/frontend-new/helper/padHelper.ts @@ -0,0 +1,155 @@ +import {Frame, Locator, Page} from "@playwright/test"; +import {MapArrayType} from "../../../node/types/MapType"; +import {randomInt} from "node:crypto"; + +export const getPadOuter = async (page: Page): Promise => { + return page.frame('ace_outer')!; +} + +export const getPadBody = async (page: Page): Promise => { + return page.frame('ace_inner')!.locator('#innerdocbody') +} + +export const selectAllText = async (page: Page) => { + await page.keyboard.down('Control'); + await page.keyboard.press('A'); + await page.keyboard.up('Control'); +} + +export const toggleUserList = async (page: Page) => { + await page.locator("button[data-l10n-id='pad.toolbar.showusers.title']").click() +} + +export const setUserName = async (page: Page, userName: string) => { + await page.waitForSelector('[class="popup popup-show"]') + await page.click("input[data-l10n-id='pad.userlist.entername']"); + await page.keyboard.type(userName); +} + + +export const showChat = async (page: Page) => { + const chatIcon = page.locator("#chaticon") + const classes = await chatIcon.getAttribute('class') + if (classes && !classes.includes('visible')) return + await chatIcon.click() + await page.waitForFunction(`!document.querySelector('#chaticon').classList.contains('visible')`) +} + +export const getCurrentChatMessageCount = async (page: Page) => { + return await page.locator('#chattext').locator('p').count() +} + +export const getChatUserName = async (page: Page) => { + return await page.locator('#chattext') + .locator('p') + .locator('b') + .innerText() +} + +export const getChatMessage = async (page: Page) => { + return (await page.locator('#chattext') + .locator('p') + .textContent({}))! + .split(await getChatTime(page))[1] + +} + + +export const getChatTime = async (page: Page) => { + return await page.locator('#chattext') + .locator('p') + .locator('.time') + .innerText() +} + +export const sendChatMessage = async (page: Page, message: string) => { + let currentChatCount = await getCurrentChatMessageCount(page) + + const chatInput = page.locator('#chatinput') + await chatInput.click() + await page.keyboard.type(message) + await page.keyboard.press('Enter') + if(message === "") return + await page.waitForFunction(`document.querySelector('#chattext').querySelectorAll('p').length >${currentChatCount}`) +} + +export const isChatBoxShown = async (page: Page):Promise => { + const classes = await page.locator('#chatbox').getAttribute('class') + return classes !==null && classes.includes('visible') +} + +export const isChatBoxSticky = async (page: Page):Promise => { + const classes = await page.locator('#chatbox').getAttribute('class') + console.log('Chat', classes && classes.includes('stickyChat')) + return classes !==null && classes.includes('stickyChat') +} + +export const hideChat = async (page: Page) => { + if(!await isChatBoxShown(page)|| await isChatBoxSticky(page)) return + await page.locator('#titlecross').click() + await page.waitForFunction(`!document.querySelector('#chatbox').classList.contains('stickyChat')`) + +} + +export const enableStickyChatviaIcon = async (page: Page) => { + if(await isChatBoxSticky(page)) return + await page.locator('#titlesticky').click() + await page.waitForFunction(`document.querySelector('#chatbox').classList.contains('stickyChat')`) +} + +export const disableStickyChatviaIcon = async (page: Page) => { + if(!await isChatBoxSticky(page)) return + await page.locator('#titlecross').click() + await page.waitForFunction(`!document.querySelector('#chatbox').classList.contains('stickyChat')`) +} + + +export const appendQueryParams = async (page: Page, queryParameters: MapArrayType) => { + const searchParams = new URLSearchParams(page.url().split('?')[1]); + Object.keys(queryParameters).forEach((key) => { + searchParams.append(key, queryParameters[key]); + }); + await page.goto(page.url()+"?"+ searchParams.toString()); + await page.waitForSelector('iframe[name="ace_outer"]'); +} + +export const goToNewPad = async (page: Page) => { + // create a new pad before each test run + const padId = "FRONTEND_TESTS"+randomInt(0, 1000); + await page.goto('http://localhost:9001/p/'+padId); + await page.waitForSelector('iframe[name="ace_outer"]'); + return padId; +} + +export const goToPad = async (page: Page, padId: string) => { + await page.goto('http://localhost:9001/p/'+padId); + await page.waitForSelector('iframe[name="ace_outer"]'); +} + + +export const clearPadContent = async (page: Page) => { + await page.keyboard.down('Control'); + await page.keyboard.press('A'); + await page.keyboard.up('Control'); + await page.keyboard.press('Delete'); +} + +export const writeToPad = async (page: Page, text: string) => { + const body = await getPadBody(page); + await body.click(); + await page.keyboard.type(text); +} + +export const clearAuthorship = async (page: Page) => { + await page.locator("button[data-l10n-id='pad.toolbar.clearAuthorship.title']").click() +} + +export const undoChanges = async (page: Page) => { + await page.keyboard.down('Control'); + await page.keyboard.press('z'); + await page.keyboard.up('Control'); +} + +export const pressUndoButton = async (page: Page) => { + await page.locator('.buttonicon-undo').click() +} diff --git a/src/tests/frontend-new/helper/settingsHelper.ts b/src/tests/frontend-new/helper/settingsHelper.ts new file mode 100644 index 000000000..729dd48f6 --- /dev/null +++ b/src/tests/frontend-new/helper/settingsHelper.ts @@ -0,0 +1,35 @@ +import {Page} from "@playwright/test"; + +export const isSettingsShown = async (page: Page) => { + const classes = await page.locator('#settings').getAttribute('class') + return classes && classes.includes('popup-show') +} + + +export const showSettings = async (page: Page) => { + if(await isSettingsShown(page)) return + await page.locator("button[data-l10n-id='pad.toolbar.settings.title']").click() + await page.waitForFunction(`document.querySelector('#settings').classList.contains('popup-show')`) +} + +export const hideSettings = async (page: Page) => { + if(!await isSettingsShown(page)) return + await page.locator("button[data-l10n-id='pad.toolbar.settings.title']").click() + await page.waitForFunction(`!document.querySelector('#settings').classList.contains('popup-show')`) +} + +export const enableStickyChatviaSettings = async (page: Page) => { + const stickyChat = page.locator('#options-stickychat') + const checked = await stickyChat.isChecked() + if(checked) return + await stickyChat.check({force: true}) + await page.waitForSelector('#options-stickychat:checked') +} + +export const disableStickyChat = async (page: Page) => { + const stickyChat = page.locator('#options-stickychat') + const checked = await stickyChat.isChecked() + if(!checked) return + await stickyChat.uncheck({force: true}) + await page.waitForSelector('#options-stickychat:not(:checked)') +} diff --git a/src/tests/frontend-new/specs/alphabet.spec.ts b/src/tests/frontend-new/specs/alphabet.spec.ts new file mode 100644 index 000000000..fcd8f7f9d --- /dev/null +++ b/src/tests/frontend-new/specs/alphabet.spec.ts @@ -0,0 +1,27 @@ +import {expect, Page, test} from "@playwright/test"; +import {clearPadContent, getPadBody, getPadOuter, goToNewPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + +test.describe('All the alphabet works n stuff', () => { + const expectedString = 'abcdefghijklmnopqrstuvwxyz'; + + test('when you enter any char it appears right', async ({page}) => { + + // get the inner iframe + const innerFrame = await getPadBody(page!); + + await innerFrame.click(); + + // delete possible old content + await clearPadContent(page!); + + + await page.keyboard.type(expectedString); + const text = await innerFrame.locator('div').innerText(); + expect(text).toBe(expectedString); + }); +}); diff --git a/src/tests/frontend-new/specs/bold.spec.ts b/src/tests/frontend-new/specs/bold.spec.ts new file mode 100644 index 000000000..6c1769da2 --- /dev/null +++ b/src/tests/frontend-new/specs/bold.spec.ts @@ -0,0 +1,50 @@ +import {expect, test} from "@playwright/test"; +import {randomInt} from "node:crypto"; +import {getPadBody, goToNewPad, selectAllText} from "../helper/padHelper"; +import exp from "node:constants"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('bold button', ()=>{ + + test('makes text bold on click', async ({page}) => { +// get the inner iframe + const innerFrame = await getPadBody(page); + + await innerFrame.click() + // Select pad text + await selectAllText(page); + await page.keyboard.type("Hi Etherpad"); + await selectAllText(page); + + // click the bold button + await page.locator("button[data-l10n-id='pad.toolbar.bold.title']").click(); + + + // check if the text is bold + expect(await innerFrame.locator('b').innerText()).toBe('Hi Etherpad'); + }) + + test('makes text bold on keypress', async ({page}) => { + // get the inner iframe + const innerFrame = await getPadBody(page); + + await innerFrame.click() + // Select pad text + await selectAllText(page); + await page.keyboard.type("Hi Etherpad"); + await selectAllText(page); + + // Press CTRL + B + await page.keyboard.down('Control'); + await page.keyboard.press('b'); + await page.keyboard.up('Control'); + + + // check if the text is bold + expect(await innerFrame.locator('b').innerText()).toBe('Hi Etherpad'); + }) + +}) diff --git a/src/tests/frontend-new/specs/change_user_name.spec.ts b/src/tests/frontend-new/specs/change_user_name.spec.ts new file mode 100644 index 000000000..bf7ea95c3 --- /dev/null +++ b/src/tests/frontend-new/specs/change_user_name.spec.ts @@ -0,0 +1,35 @@ +import {expect, test} from "@playwright/test"; +import {randomInt} from "node:crypto"; +import {goToNewPad, sendChatMessage, setUserName, showChat, toggleUserList} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + + +test("Remembers the username after a refresh", async ({page}) => { + await toggleUserList(page); + await setUserName(page,'😃') + await toggleUserList(page) + + await page.reload(); + await toggleUserList(page); + const usernameField = page.locator("input[data-l10n-id='pad.userlist.entername']"); + await expect(usernameField).toHaveValue('😃'); +}) + + +test('Own user name is shown when you enter a chat', async ({page})=> { + const chatMessage = 'O hi'; + + await toggleUserList(page); + await setUserName(page,'😃'); + await toggleUserList(page); + + await showChat(page); + await sendChatMessage(page,chatMessage); + const chatText = await page.locator('#chattext').locator('p').innerText(); + expect(chatText).toContain('😃') + expect(chatText).toContain(chatMessage) +}); diff --git a/src/tests/frontend-new/specs/chat.spec.ts b/src/tests/frontend-new/specs/chat.spec.ts new file mode 100644 index 000000000..4d4f1bd1c --- /dev/null +++ b/src/tests/frontend-new/specs/chat.spec.ts @@ -0,0 +1,116 @@ +import {expect, test} from "@playwright/test"; +import {randomInt} from "node:crypto"; +import { + appendQueryParams, + disableStickyChatviaIcon, + enableStickyChatviaIcon, + getChatMessage, + getChatTime, + getChatUserName, + getCurrentChatMessageCount, goToNewPad, hideChat, isChatBoxShown, isChatBoxSticky, + sendChatMessage, + showChat, +} from "../helper/padHelper"; +import {disableStickyChat, enableStickyChatviaSettings, hideSettings, showSettings} from "../helper/settingsHelper"; + + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + + +test('opens chat, sends a message, makes sure it exists on the page and hides chat', async ({page}) => { + const chatValue = "JohnMcLear" + + // Open chat + await showChat(page); + await sendChatMessage(page, chatValue); + + expect(await getCurrentChatMessageCount(page)).toBe(1); + const username = await getChatUserName(page) + const time = await getChatTime(page) + const chatMessage = await getChatMessage(page) + + expect(username).toBe('unnamed:'); + const regex = new RegExp('^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$'); + expect(time).toMatch(regex); + expect(chatMessage).toBe(" "+chatValue); +}) + +test("makes sure that an empty message can't be sent", async function ({page}) { + const chatValue = 'mluto'; + + await showChat(page); + + await sendChatMessage(page,""); + // Send a message + await sendChatMessage(page,chatValue); + + expect(await getCurrentChatMessageCount(page)).toBe(1); + + // check that the received message is not the empty one + const username = await getChatUserName(page) + const time = await getChatTime(page); + const chatMessage = await getChatMessage(page); + + expect(username).toBe('unnamed:'); + const regex = new RegExp('^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$'); + expect(time).toMatch(regex); + expect(chatMessage).toBe(" "+chatValue); +}); + +test('makes chat stick to right side of the screen via settings, remove sticky via settings, close it', async ({page}) =>{ + await showSettings(page); + + await enableStickyChatviaSettings(page); + expect(await isChatBoxShown(page)).toBe(true); + expect(await isChatBoxSticky(page)).toBe(true); + + await disableStickyChat(page); + expect(await isChatBoxShown(page)).toBe(true); + expect(await isChatBoxSticky(page)).toBe(false); + await hideSettings(page); + await hideChat(page); + expect(await isChatBoxShown(page)).toBe(false); + expect(await isChatBoxSticky(page)).toBe(false); +}); + +test('makes chat stick to right side of the screen via icon on the top right, ' + + 'remove sticky via icon, close it', async function ({page}) { + await showChat(page); + + await enableStickyChatviaIcon(page); + expect(await isChatBoxShown(page)).toBe(true); + expect(await isChatBoxSticky(page)).toBe(true); + + await disableStickyChatviaIcon(page); + expect(await isChatBoxShown(page)).toBe(true); + expect(await isChatBoxSticky(page)).toBe(false); + + await hideChat(page); + expect(await isChatBoxSticky(page)).toBe(false); + expect(await isChatBoxShown(page)).toBe(false); +}); + + +test('Checks showChat=false URL Parameter hides chat then' + + ' when removed it shows chat', async function ({page}) { + + // get a new pad, but don't clear the cookies + await appendQueryParams(page, { + showChat: 'false' + }); + + const chaticon = page.locator('#chaticon') + + + // chat should be hidden. + expect(await chaticon.isVisible()).toBe(false); + + // get a new pad, but don't clear the cookies + await goToNewPad(page); + const secondChatIcon = page.locator('#chaticon') + + // chat should be visible. + expect(await secondChatIcon.isVisible()).toBe(true) +}); diff --git a/src/tests/frontend-new/specs/clear_authorship_color.spec.ts b/src/tests/frontend-new/specs/clear_authorship_color.spec.ts new file mode 100644 index 000000000..6a999a57e --- /dev/null +++ b/src/tests/frontend-new/specs/clear_authorship_color.spec.ts @@ -0,0 +1,87 @@ +import {expect, test} from "@playwright/test"; +import { + clearAuthorship, + clearPadContent, + getPadBody, + goToNewPad, pressUndoButton, + selectAllText, + undoChanges, + writeToPad +} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + +test('clear authorship color', async ({page}) => { + // get the inner iframe + const innerFrame = await getPadBody(page); + const padText = "Hello" + + // type some text + await clearPadContent(page); + await writeToPad(page, padText); + const retrievedClasses = await innerFrame.locator('div span').nth(0).getAttribute('class') + expect(retrievedClasses).toContain('author'); + + // select the text + await innerFrame.click() + await selectAllText(page); + + await clearAuthorship(page); + // does the first div include an author class? + const firstDivClass = await innerFrame.locator('div').nth(0).getAttribute('class'); + expect(firstDivClass).not.toContain('author'); + const classes = page.locator('div.disconnected') + expect(await classes.isVisible()).toBe(false) +}) + + +test("makes text clear authorship colors and checks it can't be undone", async function ({page}) { + const innnerPad = await getPadBody(page); + const padText = "Hello" + + // type some text + await clearPadContent(page); + await writeToPad(page, padText); + + // get the first text element out of the inner iframe + const firstDivClass = innnerPad.locator('div').nth(0) + const retrievedClasses = await innnerPad.locator('div span').nth(0).getAttribute('class') + expect(retrievedClasses).toContain('author'); + + + await firstDivClass.focus() + await clearAuthorship(page); + expect(await firstDivClass.getAttribute('class')).not.toContain('author'); + + await undoChanges(page); + const changedFirstDiv = innnerPad.locator('div').nth(0) + expect(await changedFirstDiv.getAttribute('class')).not.toContain('author'); + + + await pressUndoButton(page); + const secondChangedFirstDiv = innnerPad.locator('div').nth(0) + expect(await secondChangedFirstDiv.getAttribute('class')).not.toContain('author'); +}); + + +// Test for https://github.com/ether/etherpad-lite/issues/5128 +test('clears authorship when first line has line attributes', async function ({page}) { + // Make sure there is text with author info. The first line must have a line attribute. + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page); + await writeToPad(page,'Hello') + await page.locator('.buttonicon-insertunorderedlist').click(); + const retrievedClasses = await padBody.locator('div span').nth(0).getAttribute('class') + expect(retrievedClasses).toContain('author'); + await padBody.click() + await selectAllText(page); + await clearAuthorship(page); + const retrievedClasses2 = await padBody.locator('div span').nth(0).getAttribute('class') + expect(retrievedClasses2).not.toContain('author'); + + expect(await page.locator('[class*="author-"]').count()).toBe(0) +}); diff --git a/src/tests/frontend-new/specs/collab_client.spec.ts b/src/tests/frontend-new/specs/collab_client.spec.ts new file mode 100644 index 000000000..5cc9c1ec3 --- /dev/null +++ b/src/tests/frontend-new/specs/collab_client.spec.ts @@ -0,0 +1,94 @@ +import {clearPadContent, getPadBody, goToNewPad, goToPad, writeToPad} from "../helper/padHelper"; +import {expect, Page, test} from "@playwright/test"; + +let padId = ""; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + padId = await goToNewPad(page); + const body = await getPadBody(page); + await body.click(); + await clearPadContent(page); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); + await writeToPad(page, "Hello World"); + await page.keyboard.press('Enter'); +}) + +test.describe('Messages in the COLLABROOM', function () { + const user1Text = 'text created by user 1'; + const user2Text = 'text created by user 2'; + + const replaceLineText = async (lineNumber: number, newText: string, page: Page) => { + const body = await getPadBody(page) + + const div = body.locator('div').nth(lineNumber) + + // simulate key presses to delete content + await div.locator('span').selectText() // select all + await page.keyboard.press('Backspace') // clear the first line + await page.keyboard.type(newText) // insert the string + }; + + test('bug #4978 regression test', async function ({browser}) { + // The bug was triggered by receiving a change from another user while simultaneously composing + // a character and waiting for an acknowledgement of a previously sent change. + + // User 1 + const context1 = await browser.newContext(); + const page1 = await context1.newPage(); + await goToPad(page1, padId) + const body1 = await getPadBody(page1) + // Perform actions as User 1... + + // User 2 + const context2 = await browser.newContext(); + const page2 = await context2.newPage(); + await goToPad(page2, padId) + const body2 = await getPadBody(page1) + + await replaceLineText(0, user1Text,page1); + + const text = await body2.locator('div').nth(0).textContent() + const res = text === user1Text + expect(res).toBe(true) + + // User 1 starts a character composition. + + + await replaceLineText(1, user2Text, page2) + + await expect(body1.locator('div').nth(1)).toHaveText(user2Text) + + + // Users 1 and 2 make some more changes. + await replaceLineText(3, user2Text, page2); + + await expect(body1.locator('div').nth(3)).toHaveText(user2Text) + + await replaceLineText(2, user1Text, page1); + await expect(body2.locator('div').nth(2)).toHaveText(user1Text) + + // All changes should appear in both views. + const expectedLines = [ + user1Text, + user2Text, + user1Text, + user2Text, + ]; + + for (let i=0;i{ + // create a new pad before each test run + await goToNewPad(page); +}) + + +test('delete keystroke', async ({page}) => { + const padText = "Hello World this is a test" + const body = await getPadBody(page) + await body.click() + await clearPadContent(page) + await page.keyboard.type(padText) + // Navigate to the end of the text + await page.keyboard.press('End'); + // Delete the last character + await page.keyboard.press('Backspace'); + const text = await body.locator('div').innerText(); + expect(text).toBe(padText.slice(0, -1)); +}) diff --git a/src/tests/frontend-new/specs/embed_value.spec.ts b/src/tests/frontend-new/specs/embed_value.spec.ts new file mode 100644 index 000000000..14e380a55 --- /dev/null +++ b/src/tests/frontend-new/specs/embed_value.spec.ts @@ -0,0 +1,146 @@ +import {expect, Page, test} from "@playwright/test"; +import {goToNewPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); +}) + +test.describe('embed links', function () { + const objectify = function (str: string) { + const hash = {}; + const parts = str.split('&'); + for (let i = 0; i < parts.length; i++) { + const keyValue = parts[i].split('='); + // @ts-ignore + hash[keyValue[0]] = keyValue[1]; + } + return hash; + }; + + const checkiFrameCode = async function (embedCode: string, readonly: boolean, page: Page) { + // turn the code into an html element + + await page.setContent(embedCode, {waitUntil: 'load'}) + const locator = page.locator('body').locator('iframe').last() + + + // read and check the frame attributes + const width = await locator.getAttribute('width'); + const height = await locator.getAttribute('height'); + const name = await locator.getAttribute('name'); + expect(width).toBe('100%'); + expect(height).toBe('600'); + expect(name).toBe(readonly ? 'embed_readonly' : 'embed_readwrite'); + + // parse the url + const src = (await locator.getAttribute('src'))!; + const questionMark = src.indexOf('?'); + const url = src.substring(0, questionMark); + const paramsStr = src.substring(questionMark + 1); + const params = objectify(paramsStr); + + const expectedParams = { + showControls: 'true', + showChat: 'true', + showLineNumbers: 'true', + useMonospaceFont: 'false', + }; + + // check the url + if (readonly) { + expect(url.indexOf('r.') > 0).toBe(true); + } else { + expect(url).toBe(await page.evaluate(() => window.location.href)); + } + + // check if all parts of the url are like expected + expect(params).toEqual(expectedParams); + }; + + test.describe('read and write', function () { + test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); + }) + test.describe('the share link', function () { + test('is the actual pad url', async function ({page}) { + + const shareButton = page.locator('.buttonicon-embed') + // open share dropdown + await shareButton.click() + + // get the link of the share field + the actual pad url and compare them + const shareLink = await page.locator('#linkinput').inputValue() + const padURL = page.url(); + expect(shareLink).toBe(padURL); + }); + }); + + test.describe('the embed as iframe code', function () { + test('is an iframe with the the correct url parameters and correct size', async function ({page}) { + + const shareButton = page.locator('.buttonicon-embed') + await shareButton.click() + + // get the link of the share field + the actual pad url and compare them + const embedCode = await page.locator('#embedinput').inputValue() + + + await checkiFrameCode(embedCode, false, page); + }); + }); + }); + + test.describe('when read only option is set', function () { + test.beforeEach(async ({ page })=>{ + // create a new pad before each test run + await goToNewPad(page); + }) + + test.describe('the share link', function () { + test('shows a read only url', async function ({page}) { + + // open share dropdown + const shareButton = page.locator('.buttonicon-embed') + await shareButton.click() + const readonlyCheckbox = page.locator('#readonlyinput') + await readonlyCheckbox.click({ + force: true + }) + await page.waitForSelector('#readonlyinput:checked') + + // get the link of the share field + the actual pad url and compare them + const shareLink = await page.locator('#linkinput').inputValue() + const containsReadOnlyLink = shareLink.indexOf('r.') > 0; + expect(containsReadOnlyLink).toBe(true); + }); + }); + + test.describe('the embed as iframe code', function () { + test('is an iframe with the the correct url parameters and correct size', async function ({page}) { + + + // open share dropdown + const shareButton = page.locator('.buttonicon-embed') + await shareButton.click() + + // check read only checkbox, a bit hacky + const readonlyCheckbox = page.locator('#readonlyinput') + await readonlyCheckbox.click({ + force: true + }) + + await page.waitForSelector('#readonlyinput:checked') + + + // get the link of the share field + the actual pad url and compare them + const embedCode = await page.locator('#embedinput').inputValue() + + await checkiFrameCode(embedCode, true, page); + }); + }); + + }) + +}) diff --git a/src/tests/frontend-new/specs/enter.spec.ts b/src/tests/frontend-new/specs/enter.spec.ts new file mode 100644 index 000000000..fd9c732c2 --- /dev/null +++ b/src/tests/frontend-new/specs/enter.spec.ts @@ -0,0 +1,63 @@ +'use strict'; +import {expect, test} from "@playwright/test"; +import {getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('enter keystroke', function () { + + test('creates a new line & puts cursor onto a new line', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + const firstTextElement = padBody.locator('div').nth(0) + + // get the original string value minus the last char + const originalTextValue = await firstTextElement.textContent(); + + // simulate key presses to enter content + await firstTextElement.click() + await page.keyboard.press('Home'); + await page.keyboard.press('Enter'); + + const updatedFirstElement = padBody.locator('div').nth(0) + expect(await updatedFirstElement.textContent()).toBe('') + + const newSecondLine = padBody.locator('div').nth(1); + // expect the second line to be the same as the original first line. + expect(await newSecondLine.textContent()).toBe(originalTextValue); + }); + + test('enter is always visible after event', async function ({page}) { + const padBody = await getPadBody(page); + const originalLength = await padBody.locator('div').count(); + let lastLine = padBody.locator('div').last(); + + // simulate key presses to enter content + let i = 0; + const numberOfLines = 15; + while (i < numberOfLines) { + lastLine = padBody.locator('div').last(); + await lastLine.focus(); + await page.keyboard.press('End'); + await page.keyboard.press('Enter'); + + // check we can see the caret.. + i++; + } + + expect(await padBody.locator('div').count()).toBe(numberOfLines + originalLength); + + // is edited line fully visible? + const lastDiv = padBody.locator('div').last() + const lastDivOffset = await lastDiv.boundingBox(); + const bottomOfLastLine = lastDivOffset!.y + lastDivOffset!.height; + const scrolledWindow = page.frames()[0]; + const windowOffset = await scrolledWindow.evaluate(() => window.pageYOffset); + const windowHeight = await scrolledWindow.evaluate(() => window.innerHeight); + + expect(windowOffset + windowHeight).toBeGreaterThan(bottomOfLastLine); + }); +}); diff --git a/src/tests/frontend-new/specs/indentation.spec.ts b/src/tests/frontend-new/specs/indentation.spec.ts new file mode 100644 index 000000000..269a67011 --- /dev/null +++ b/src/tests/frontend-new/specs/indentation.spec.ts @@ -0,0 +1,241 @@ +import {expect, test} from "@playwright/test"; +import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper"; + +test.beforeEach(async ({ page })=>{ + await goToNewPad(page); +}) + +test.describe('indentation button', function () { + test('indent text with keypress', async function ({page}) { + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + const $firstTextElement = padBody.locator('div').first(); + + // select this text element + await $firstTextElement.selectText() + + await page.keyboard.press('Tab'); + + const uls = padBody.locator('div').first().locator('ul li') + await expect(uls).toHaveCount(1); + }); + + test('indent text with button', async function ({page}) { + const padBody = await getPadBody(page); + await page.locator('.buttonicon-indent').click() + + const uls = padBody.locator('div').first().locator('ul li') + await expect(uls).toHaveCount(1); + }); + + + test('keeps the indent on enter for the new line', async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + await page.locator('.buttonicon-indent').click() + + // type a bit, make a line break and type again + await padBody.locator('div').first().focus() + await page.keyboard.type('line 1') + await page.keyboard.press('Enter'); + await page.keyboard.type('line 2') + await page.keyboard.press('Enter'); + + const $newSecondLine = padBody.locator('div span').nth(1) + + const hasULElement = padBody.locator('ul li') + + await expect(hasULElement).toHaveCount(3); + await expect($newSecondLine).toHaveText('line 2'); + }); + + + test('indents text with spaces on enter if previous line ends ' + + "with ':', '[', '(', or '{'", async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + // type a bit, make a line break and type again + const $firstTextElement = padBody.locator('div').first(); + await writeToPad(page, "line with ':'"); + await page.keyboard.press('Enter'); + await writeToPad(page, "line with '['"); + await page.keyboard.press('Enter'); + await writeToPad(page, "line with '('"); + await page.keyboard.press('Enter'); + await writeToPad(page, "line with '{{}'"); + + await expect(padBody.locator('div').nth(3)).toHaveText("line with '{{}'"); + + // we validate bottom to top for easier implementation + + + // curly braces + const $lineWithCurlyBraces = padBody.locator('div').nth(3) + await $lineWithCurlyBraces.click(); + await page.keyboard.press('End'); + await page.keyboard.type('{{'); + + // cannot use sendkeys('{enter}') here, browser does not read the command properly + await page.keyboard.press('Enter'); + + expect(await padBody.locator('div').nth(4).textContent()).toMatch(/\s{4}/); // tab === 4 spaces + + + + // parenthesis + const $lineWithParenthesis = padBody.locator('div').nth(2) + await $lineWithParenthesis.click(); + await page.keyboard.press('End'); + await page.keyboard.type('('); + await page.keyboard.press('Enter'); + const $lineAfterParenthesis = padBody.locator('div').nth(3) + expect(await $lineAfterParenthesis.textContent()).toMatch(/\s{4}/); + + // bracket + const $lineWithBracket = padBody.locator('div').nth(1) + await $lineWithBracket.click(); + await page.keyboard.press('End'); + await page.keyboard.type('['); + await page.keyboard.press('Enter'); + const $lineAfterBracket = padBody.locator('div').nth(2); + expect(await $lineAfterBracket.textContent()).toMatch(/\s{4}/); + + // colon + const $lineWithColon = padBody.locator('div').first(); + await $lineWithColon.click(); + await page.keyboard.press('End'); + await page.keyboard.type(':'); + await page.keyboard.press('Enter'); + const $lineAfterColon = padBody.locator('div').nth(1); + expect(await $lineAfterColon.textContent()).toMatch(/\s{4}/); + }); + + test('appends indentation to the indent of previous line if previous line ends ' + + "with ':', '[', '(', or '{'", async function ({page}) { + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + // type a bit, make a line break and type again + await writeToPad(page, " line with some indentation and ':'") + await page.keyboard.press('Enter'); + await writeToPad(page, "line 2") + + const $lineWithColon = padBody.locator('div').first(); + await $lineWithColon.click(); + await page.keyboard.press('End'); + await page.keyboard.type(':'); + await page.keyboard.press('Enter'); + + const $lineAfterColon = padBody.locator('div').nth(1); + // previous line indentation + regular tab (4 spaces) + expect(await $lineAfterColon.textContent()).toMatch(/\s{6}/); + }); + + test("issue #2772 shows '*' when multiple indented lines " + + ' receive a style and are outdented', async function ({page}) { + + const padBody = await getPadBody(page); + await padBody.click() + await clearPadContent(page) + + const inner = padBody.locator('div').first(); + // make sure pad has more than one line + await inner.click() + await page.keyboard.type('First'); + await page.keyboard.press('Enter'); + await page.keyboard.type('Second'); + + + // indent first 2 lines + await padBody.locator('div').nth(0).selectText(); + await page.locator('.buttonicon-indent').click() + + await padBody.locator('div').nth(1).selectText(); + await page.locator('.buttonicon-indent').click() + + + await expect(padBody.locator('ul li')).toHaveCount(2); + + + // apply bold + await padBody.locator('div').nth(0).selectText(); + await page.locator('.buttonicon-bold').click() + + await padBody.locator('div').nth(1).selectText(); + await page.locator('.buttonicon-bold').click() + + await expect(padBody.locator('div b')).toHaveCount(2); + + // outdent first 2 lines + await padBody.locator('div').nth(0).selectText(); + await page.locator('.buttonicon-outdent').click() + + await padBody.locator('div').nth(1).selectText(); + await page.locator('.buttonicon-outdent').click() + + await expect(padBody.locator('ul li')).toHaveCount(0); + + // check if '*' is displayed + const secondLine = padBody.locator('div').nth(1); + await expect(secondLine).toHaveText('Second'); + }); + + test('makes text indented and outdented', async function ({page}) { + // get the inner iframe + + const padBody = await getPadBody(page); + + // get the first text element out of the inner iframe + let firstTextElement = padBody.locator('div').first(); + + // select this text element + await firstTextElement.selectText() + + // get the indentation button and click it + await page.locator('.buttonicon-indent').click() + + let newFirstTextElement = padBody.locator('div').first(); + + // is there a list-indent class element now? + await expect(newFirstTextElement.locator('ul')).toHaveCount(1); + + await expect(newFirstTextElement.locator('li')).toHaveCount(1); + + // indent again + await page.locator('.buttonicon-indent').click() + + newFirstTextElement = padBody.locator('div').first(); + + + // is there a list-indent class element now? + const ulList = newFirstTextElement.locator('ul').first() + await expect(ulList).toHaveCount(1); + // expect it to be part of a list + expect(await ulList.getAttribute('class')).toBe('list-indent2'); + + // make sure the text hasn't changed + expect(await newFirstTextElement.textContent()).toBe(await firstTextElement.textContent()); + + + // test outdent + + // get the unindentation button and click it twice + newFirstTextElement = padBody.locator('div').first(); + await newFirstTextElement.selectText() + await page.locator('.buttonicon-outdent').click() + await page.locator('.buttonicon-outdent').click() + + newFirstTextElement = padBody.locator('div').first(); + + // is there a list-indent class element now? + await expect(newFirstTextElement.locator('ul')).toHaveCount(0); + + // make sure the text hasn't changed + expect(await newFirstTextElement.textContent()).toEqual(await firstTextElement.textContent()); + }); +}); diff --git a/src/tests/frontend/specs/adminsettings.js b/src/tests/frontend/specs/adminsettings.js deleted file mode 100644 index 7461cb661..000000000 --- a/src/tests/frontend/specs/adminsettings.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -describe('Admin > Settings', function () { - this.timeout(480000); - - before(async function () { - let success = false; - $.ajax({ - url: `${location.protocol}//admin:changeme@${location.hostname}:${location.port}/admin/`, - type: 'GET', - success: () => success = true, - }); - await helper.waitForPromise(() => success === true); - }); - - beforeEach(async function () { - helper.newAdmin('settings'); - // needed, because the load event is fired to early - await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.settings').val().length > 0, 5000); - }); - - it('Are Settings visible, populated, does save work', async function () { - const save = async () => { - const p = new Promise((resolve) => { - const observer = new MutationObserver(() => { resolve(); observer.disconnect(); }); - observer.observe( - helper.admin$('#response')[0], {attributes: true, childList: false, subtree: false}); - }); - helper.admin$('#saveSettings').trigger('click'); - await p; - }; - - // save old value - const settings = helper.admin$('.settings').val(); - const settingsLength = settings.length; - - // set new value - helper.admin$('.settings').val((_, text) => `/* test */\n${text}`); - await helper.waitForPromise( - () => settingsLength + 11 === helper.admin$('.settings').val().length, 5000); - await save(); - - // new value for settings.json should now be saved - // reset it to the old value - helper.newAdmin('settings'); - await helper.waitForPromise( - () => helper.admin$ && - helper.admin$('.settings').val().length === settingsLength + 11, 20000); - - // replace the test value with a line break - helper.admin$('.settings').val((_, text) => text.replace('/* test */\n', '')); - await helper.waitForPromise(() => settingsLength === helper.admin$('.settings').val().length); - await save(); - - // settings should have the old value - helper.newAdmin('settings'); - await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.settings').val().length === settingsLength && - settings === helper.admin$('.settings').val(), 20000); - }); - - it('restart works', async function () { - const getStartTime = async () => { - try { - const {httpStartTime} = await $.ajax({ - url: new URL('/stats', window.location.href), - method: 'GET', - dataType: 'json', - timeout: 450, // Slightly less than the waitForPromise() interval. - }); - return httpStartTime; - } catch (err) { - document.getElementById('console').append( - `an error occurred: ${err.message} of type ${err.name}\n`); - return null; - } - }; - let oldStartTime; - await helper.waitForPromise(async () => { - oldStartTime = await getStartTime(); - return oldStartTime != null && oldStartTime > 0; - }, 2100, 500); - helper.admin$('#restartEtherpad').trigger('click'); - await helper.waitForPromise(async () => { - const startTime = await getStartTime(); - return startTime != null && startTime > oldStartTime; - }, 60000, 500); - }); -}); diff --git a/src/tests/frontend/specs/admintroubleshooting.js b/src/tests/frontend/specs/admintroubleshooting.js deleted file mode 100755 index 6e428d3b1..000000000 --- a/src/tests/frontend/specs/admintroubleshooting.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -describe('Admin Troupbleshooting page', function () { - before(async function () { - let success = false; - $.ajax({ - url: `${location.protocol}//admin:changeme@${location.hostname}:${location.port}/admin`, - type: 'GET', - success: () => success = true, - }); - await helper.waitForPromise(() => success === true); - }); - - // create a new pad before each test run - beforeEach(async function () { - helper.newAdmin('plugins/info'); - await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.menu').find('li').length >= 3); - }); - - it('Shows Troubleshooting page Manager', async function () { - helper.admin$('a[data-l10n-id="admin_plugins_info"]')[0].click(); - }); - - it('Shows a version number', async function () { - const content = helper.admin$('span[data-l10n-id="admin_plugins_info.version_number"]') - .parent().text(); - const version = content.split(': ')[1].split('.'); - if (version.length !== 3) { - throw new Error('Not displaying a semver version number'); - } - }); - - it('Lists installed parts', async function () { - const parts = helper.admin$('pre')[1]; - if (parts.textContent.indexOf('ep_etherpad-lite/adminsettings') === -1) { - throw new Error('No admin setting part being displayed...'); - } - }); - - it('Lists installed hooks', async function () { - const parts = helper.admin$('dt'); - if (parts.length <= 20) { - throw new Error('Not enough hooks being displayed...'); - } - }); -}); diff --git a/src/tests/frontend/specs/adminupdateplugins.js b/src/tests/frontend/specs/adminupdateplugins.js deleted file mode 100755 index 1f6e21fc8..000000000 --- a/src/tests/frontend/specs/adminupdateplugins.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; - -describe('Plugins page', function () { - function timeout(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - - before(async function () { - let success = false; - $.ajax({ - url: `${location.protocol}//admin:changeme@${location.hostname}:${location.port}/admin`, - type: 'GET', - success: () => success = true, - }); - await helper.waitForPromise(() => success === true); - }); - - // create a new pad before each test run - beforeEach(async function () { - helper.newAdmin('plugins'); - await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.menu').find('li').length >= 3, 30000); - }); - - it('Lists some plugins', async function () { - await helper.waitForPromise(() => helper.admin$('.results').children().length > 50, 20000); - }); - - it('Searches for plugin', async function () { - helper.admin$('#search-query').val('ep_font_color'); - await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 10000); - await helper.waitForPromise(() => helper.admin$('.results').children().length < 300, 10000); - }); - - it('Attempt to Update a plugin', async function () { - this.timeout(280000); - - await helper.waitForPromise(() => helper.admin$('.results').children().length > 50, 20000); - - if (helper.admin$('.ep_align').length === 0) this.skip(); - - await helper.waitForPromise( - () => helper.admin$('.ep_align .version').text().split('.').length >= 2); - - const minorVersionBefore = - parseInt(helper.admin$('.ep_align .version').text().split('.')[1]); - - if (!minorVersionBefore) { - throw new Error('Unable to get minor number of plugin, is the plugin installed?'); - } - - if (minorVersionBefore !== 2) this.skip(); - - helper.waitForPromise( - () => helper.admin$('.ep_align .do-update').length === 1); - - await timeout(500); // HACK! Please submit better fix.. - const $doUpdateButton = helper.admin$('.ep_align .do-update'); - $doUpdateButton.trigger('click'); - - // ensure its showing as Updating - await helper.waitForPromise( - () => helper.admin$('.ep_align .message').text() === 'Updating'); - - // Ensure it's a higher minor version IE 0.3.x as 0.2.x was installed - // Coverage for https://github.com/ether/etherpad-lite/issues/4536 - await helper.waitForPromise(() => parseInt(helper.admin$('.ep_align .version') - .text() - .split('.')[1]) > minorVersionBefore, 60000, 1000); - // allow 50 seconds, check every 1 second. - }); - it('Attempt to Install a plugin', async function () { - this.timeout(280000); - - helper.admin$('#search-query').val('ep_headings2'); - await helper.waitForPromise(() => helper.admin$('.results').children().length < 300, 6000); - await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 6000); - - // skip if we already have ep_headings2 installed.. - if (helper.admin$('.ep_headings2 .do-install').is(':visible') === false) this.skip(); - - helper.admin$('.ep_headings2 .do-install').trigger('click'); - // ensure install has attempted to be started - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .do-install').length !== 0, 120000); - // ensure its not showing installing any more - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .message').text() === '', 180000); - // ensure uninstall button is visible - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .do-uninstall').length !== 0, 120000); - }); - - it('Attempt to Uninstall a plugin', async function () { - this.timeout(360000); - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .do-uninstall').length !== 0, 120000); - - helper.admin$('.ep_headings2 .do-uninstall').trigger('click'); - - // ensure its showing uninstalling - await helper.waitForPromise( - () => helper.admin$('.ep_headings2 .message') - .text() === 'Uninstalling', 120000); - // ensure its gone - await helper.waitForPromise( - () => helper.admin$('.ep_headings2').length === 0, 240000); - - helper.admin$('#search-query').val('ep_font'); - await helper.waitForPromise(() => helper.admin$('.results').children().length < 300, 240000); - await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 1000); - }); -}); diff --git a/src/tests/frontend/specs/alphabet.js b/src/tests/frontend/specs/alphabet.js deleted file mode 100644 index 999cfdf3a..000000000 --- a/src/tests/frontend/specs/alphabet.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -describe('All the alphabet works n stuff', function () { - const expectedString = 'abcdefghijklmnopqrstuvwxyz'; - - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('when you enter any char it appears right', function (done) { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const firstTextElement = inner$('div').first(); - - // simulate key presses to delete content - firstTextElement.sendkeys('{selectall}'); // select all - firstTextElement.sendkeys('{del}'); // clear the first line - firstTextElement.sendkeys(expectedString); // insert the string - - helper.waitFor(() => inner$('div').first().text() === expectedString, 2000).done(done); - }); -}); diff --git a/src/tests/frontend/specs/bold.js b/src/tests/frontend/specs/bold.js deleted file mode 100644 index cadfb7a54..000000000 --- a/src/tests/frontend/specs/bold.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -describe('bold button', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('makes text bold on click', function (done) { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - // get the bold button and click it - const $boldButton = chrome$('.buttonicon-bold'); - $boldButton.trigger('click'); - - const $newFirstTextElement = inner$('div').first(); - - // is there a element now? - const isBold = $newFirstTextElement.find('b').length === 1; - - // expect it to be bold - expect(isBold).to.be(true); - - // make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - - done(); - }); - - it('makes text bold on keypress', function (done) { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const e = new inner$.Event(helper.evtType); - e.ctrlKey = true; // Control key - e.which = 66; // b - inner$('#innerdocbody').trigger(e); - - const $newFirstTextElement = inner$('div').first(); - - // is there a element now? - const isBold = $newFirstTextElement.find('b').length === 1; - - // expect it to be bold - expect(isBold).to.be(true); - - // make sure the text hasn't changed - expect($newFirstTextElement.text()).to.eql($firstTextElement.text()); - - done(); - }); -}); diff --git a/src/tests/frontend/specs/change_user_name.js b/src/tests/frontend/specs/change_user_name.js deleted file mode 100644 index b146a1281..000000000 --- a/src/tests/frontend/specs/change_user_name.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -describe('change username value', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('Remembers the user name after a refresh', async function () { - this.timeout(10000); - await helper.toggleUserList(); - await helper.setUserName('😃'); - // Give the server an opportunity to write the new name. - await new Promise((resolve) => setTimeout(resolve, 1000)); - // get a new pad, but don't clear the cookies - await helper.aNewPad({clearCookies: false}); - await helper.toggleUserList(); - await helper.waitForPromise(() => helper.usernameField().val() === '😃'); - }); - - it('Own user name is shown when you enter a chat', async function () { - this.timeout(10000); - await helper.toggleUserList(); - await helper.setUserName('😃'); - - await helper.showChat(); - await helper.sendChatMessage('O hi{enter}'); - - await helper.waitForPromise(() => { - // username:hours:minutes text - const chatText = helper.chatTextParagraphs().text(); - return chatText.indexOf('😃') === 0; - }); - }); -}); diff --git a/src/tests/frontend/specs/chat.js b/src/tests/frontend/specs/chat.js deleted file mode 100644 index 82527f372..000000000 --- a/src/tests/frontend/specs/chat.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -describe('Chat messages and UI', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('opens chat, sends a message, makes sure it exists ' + - 'on the page and hides chat', async function () { - this.timeout(3000); - const chatValue = 'JohnMcLear'; - - await helper.showChat(); - await helper.sendChatMessage(`${chatValue}{enter}`); - - expect(helper.chatTextParagraphs().length).to.be(1); - - //

- // unnamed: - // 12:38 - // JohnMcLear - //

- const username = helper.chatTextParagraphs().children('b').text(); - const time = helper.chatTextParagraphs().children('.time').text(); - - // TODO: The '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of it - // without breaking the other tests that use $.sendkeys(). - expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}\n`); - - await helper.hideChat(); - }); - - it("makes sure that an empty message can't be sent", async function () { - const chatValue = 'mluto'; - - await helper.showChat(); - - // simulate a keypress of typing enter, mluto and enter (to send 'mluto') - await helper.sendChatMessage(`{enter}${chatValue}{enter}`); - - const chat = helper.chatTextParagraphs(); - - expect(chat.length).to.be(1); - - // check that the received message is not the empty one - const username = chat.children('b').text(); - const time = chat.children('.time').text(); - - // TODO: Each '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of them - // without breaking the other tests that use $.sendkeys(). - expect(chat.text()).to.be(`${username}${time} \n${chatValue}\n`); - }); - - it('makes chat stick to right side of the screen via settings, ' + - 'remove sticky via settings, close it', async function () { - this.timeout(5000); - await helper.showSettings(); - - await helper.enableStickyChatviaSettings(); - expect(helper.isChatboxShown()).to.be(true); - expect(helper.isChatboxSticky()).to.be(true); - - await helper.disableStickyChatviaSettings(); - expect(helper.isChatboxSticky()).to.be(false); - expect(helper.isChatboxShown()).to.be(true); - - await helper.hideChat(); - expect(helper.isChatboxSticky()).to.be(false); - expect(helper.isChatboxShown()).to.be(false); - }); - - it('makes chat stick to right side of the screen via icon on the top' + - ' right, remove sticky via icon, close it', async function () { - this.timeout(5000); - await helper.showChat(); - - await helper.enableStickyChatviaIcon(); - expect(helper.isChatboxShown()).to.be(true); - expect(helper.isChatboxSticky()).to.be(true); - - await helper.disableStickyChatviaIcon(); - expect(helper.isChatboxShown()).to.be(true); - expect(helper.isChatboxSticky()).to.be(false); - - await helper.hideChat(); - expect(helper.isChatboxSticky()).to.be(false); - expect(helper.isChatboxShown()).to.be(false); - }); - - xit('Checks showChat=false URL Parameter hides chat then' + - ' when removed it shows chat', async function () { - // give it a second to save the username on the server side - await new Promise((resolve) => setTimeout(resolve, 3000)); - - // get a new pad, but don't clear the cookies - await helper.aNewPad({clearCookies: false, params: {showChat: 'false'}}); - - let chrome$ = helper.padChrome$; - let chaticon = chrome$('#chaticon'); - // chat should be hidden. - expect(chaticon.is(':visible')).to.be(false); - - // give it a second to save the username on the server side - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // get a new pad, but don't clear the cookies - await helper.aNewPad({clearCookies: false}); - - chrome$ = helper.padChrome$; - chaticon = chrome$('#chaticon'); - // chat should be visible. - expect(chaticon.is(':visible')).to.be(true); - }); -}); diff --git a/src/tests/frontend/specs/clear_authorship_colors.js b/src/tests/frontend/specs/clear_authorship_colors.js deleted file mode 100644 index 0dc9c7f2a..000000000 --- a/src/tests/frontend/specs/clear_authorship_colors.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; - -describe('clear authorship colors button', function () { - let padId; - - // create a new pad before each test run - beforeEach(async function () { - padId = await helper.aNewPad(); - }); - - it('makes text clear authorship colors', async function () { - this.timeout(2500); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // override the confirm dialogue functioon - helper.padChrome$.window.confirm = () => true; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // Set some new text - const sentText = 'Hello'; - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - $firstTextElement.sendkeys(sentText); - $firstTextElement.sendkeys('{rightarrow}'); - - // wait until we have the full value available - await helper.waitForPromise( - () => inner$('div span').first().attr('class').indexOf('author') !== -1); - - // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship - inner$('div').first().trigger('focus'); - - // get the clear authorship colors button and click it - const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); - $clearauthorshipcolorsButton.trigger('click'); - - // does the first div include an author class? - const hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; - expect(hasAuthorClass).to.be(false); - - await helper.waitForPromise( - () => chrome$('div.disconnected').attr('class').indexOf('visible') === -1); - }); - - it("makes text clear authorship colors and checks it can't be undone", async function () { - this.timeout(1500); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // override the confirm dialogue functioon - helper.padChrome$.window.confirm = () => true; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // Set some new text - const sentText = 'Hello'; - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - $firstTextElement.sendkeys(sentText); - $firstTextElement.sendkeys('{rightarrow}'); - - // wait until we have the full value available - await helper.waitForPromise( - () => inner$('div span').first().attr('class').indexOf('author') !== -1); - - // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship - inner$('div').first().trigger('focus'); - - // get the clear authorship colors button and click it - const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); - $clearauthorshipcolorsButton.trigger('click'); - - // does the first div include an author class? - let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; - expect(hasAuthorClass).to.be(false); - - const e = new inner$.Event(helper.evtType); - e.ctrlKey = true; // Control key - e.which = 90; // z - inner$('#innerdocbody').trigger(e); // shouldn't od anything - - // does the first div include an author class? - hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; - expect(hasAuthorClass).to.be(false); - - // get undo and redo buttons - const $undoButton = chrome$('.buttonicon-undo'); - - // click the button - $undoButton.trigger('click'); // shouldn't do anything - hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; - expect(hasAuthorClass).to.be(false); - - await helper.waitForPromise( - () => chrome$('div.disconnected').attr('class').indexOf('visible') === -1); - }); - - // Test for https://github.com/ether/etherpad-lite/issues/5128 - it('clears authorship when first line has line attributes', async function () { - // override the confirm dialogue function - helper.padChrome$.window.confirm = () => true; - - // Make sure there is text with author info. The first line must have a line attribute. - await helper.clearPad(); - await helper.edit('Hello'); - helper.padChrome$('.buttonicon-insertunorderedlist').click(); - await helper.waitForPromise(() => helper.padInner$('[class*="author-"]').length > 0); - - const nCommits = helper.commits.length; - helper.padChrome$('.buttonicon-clearauthorship').click(); - await helper.waitForPromise(() => helper.padInner$('[class*="author-"]').length === 0); - - // Make sure the change was actually accepted by reloading the pad and looking for authorship. - // Before the pad can be reloaded the server might need some time to accept the change. - await helper.waitForPromise(() => helper.commits.length > nCommits); - await helper.aNewPad({id: padId}); - expect(helper.padInner$('[class*="author-"]').length).to.be(0); - }); -}); diff --git a/src/tests/frontend/specs/collab_client.js b/src/tests/frontend/specs/collab_client.js deleted file mode 100644 index 9cc943e73..000000000 --- a/src/tests/frontend/specs/collab_client.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -describe('Messages in the COLLABROOM', function () { - const user1Text = 'text created by user 1'; - const user2Text = 'text created by user 2'; - - const triggerEvent = (eventName) => { - const event = new helper.padInner$.Event(eventName); - helper.padInner$('#innerdocbody').trigger(event); - }; - - const replaceLineText = async (lineNumber, newText) => { - const inner$ = helper.padInner$; - - // get the line element - const $line = inner$('div').eq(lineNumber); - - // simulate key presses to delete content - $line.sendkeys('{selectall}'); // select all - $line.sendkeys('{del}'); // clear the first line - $line.sendkeys(newText); // insert the string - - await helper.waitForPromise(() => inner$('div').eq(lineNumber).text() === newText); - }; - - before(async function () { - this.timeout(10000); - await helper.aNewPad(); - await helper.multipleUsers.init(); - }); - - it('bug #4978 regression test', async function () { - // The bug was triggered by receiving a change from another user while simultaneously composing - // a character and waiting for an acknowledgement of a previously sent change. - - // User 1 starts sending a change to the server. - let sendStarted; - const finishSend = (() => { - const socketJsonObj = helper.padChrome$.window.pad.socket; - const sendBackup = socketJsonObj.emit; - let startSend; - sendStarted = new Promise((resolve) => { startSend = resolve; }); - let finishSend; - const sendP = new Promise((resolve) => { finishSend = resolve; }); - socketJsonObj.send = (...args) => { - startSend(); - sendP.then(() => { - socketJsonObj.send = sendBackup; - socketJsonObj.send('message', ...args); - }); - }; - return finishSend; - })(); - await replaceLineText(0, user1Text); - await sendStarted; - - // User 1 starts a character composition. - triggerEvent('compositionstart'); - - // User 1 receives a change from user 2. (User 1 will not incorporate the change until the - // composition is completed.) - const user2ChangeArrivedAtUser1 = new Promise((resolve) => { - const cc = helper.padChrome$.window.pad.collabClient; - const origHM = cc.handleMessageFromServer; - cc.handleMessageFromServer = (evt) => { - if (evt.type === 'COLLABROOM' && evt.data.type === 'NEW_CHANGES') { - cc.handleMessageFromServer = origHM; - resolve(); - } - return origHM.call(cc, evt); - }; - }); - await helper.multipleUsers.performAsOtherUser(async () => await replaceLineText(1, user2Text)); - await user2ChangeArrivedAtUser1; - - // User 1 finishes sending the change to the server. User 2 should see the changes right away. - finishSend(); - await helper.multipleUsers.performAsOtherUser(async () => await helper.waitForPromise( - () => helper.padInner$('div').eq(0).text() === user1Text)); - - // User 1 finishes the character composition. User 2's change should then become visible. - triggerEvent('compositionend'); - await helper.waitForPromise(() => helper.padInner$('div').eq(1).text() === user2Text); - - // Users 1 and 2 make some more changes. - await helper.multipleUsers.performAsOtherUser(async () => await replaceLineText(3, user2Text)); - await replaceLineText(2, user1Text); - - // All changes should appear in both views. - const assertContent = async () => await helper.waitForPromise(() => { - const expectedLines = [ - user1Text, - user2Text, - user1Text, - user2Text, - ]; - return expectedLines.every((txt, i) => helper.padInner$('div').eq(i).text() === txt); - }); - await assertContent(); - await helper.multipleUsers.performAsOtherUser(assertContent); - }); -}); diff --git a/src/tests/frontend/specs/delete.js b/src/tests/frontend/specs/delete.js deleted file mode 100644 index 05164280b..000000000 --- a/src/tests/frontend/specs/delete.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -describe('delete keystroke', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('makes text delete', async function () { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // get the original length of this element - const elementLength = $firstTextElement.text().length; - - // simulate key presses to delete content - $firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key - $firstTextElement.sendkeys('{del}'); // simulate a keypress of delete - - const $newFirstTextElement = inner$('div').first(); - - // get the new length of this element - const newElementLength = $newFirstTextElement.text().length; - - // expect it to be one char less in length - expect(newElementLength).to.be((elementLength - 1)); - }); -}); diff --git a/src/tests/frontend/specs/embed_value.js b/src/tests/frontend/specs/embed_value.js deleted file mode 100644 index 1594fd891..000000000 --- a/src/tests/frontend/specs/embed_value.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; - -describe('embed links', function () { - const objectify = function (str) { - const hash = {}; - const parts = str.split('&'); - for (let i = 0; i < parts.length; i++) { - const keyValue = parts[i].split('='); - hash[keyValue[0]] = keyValue[1]; - } - return hash; - }; - - const checkiFrameCode = function (embedCode, readonly) { - // turn the code into an html element - const $embediFrame = $(embedCode); - - // read and check the frame attributes - const width = $embediFrame.attr('width'); - const height = $embediFrame.attr('height'); - const name = $embediFrame.attr('name'); - expect(width).to.be('100%'); - expect(height).to.be('600'); - expect(name).to.be(readonly ? 'embed_readonly' : 'embed_readwrite'); - - // parse the url - const src = $embediFrame.attr('src'); - const questionMark = src.indexOf('?'); - const url = src.substr(0, questionMark); - const paramsStr = src.substr(questionMark + 1); - const params = objectify(paramsStr); - - const expectedParams = { - showControls: 'true', - showChat: 'true', - showLineNumbers: 'true', - useMonospaceFont: 'false', - }; - - // check the url - if (readonly) { - expect(url.indexOf('r.') > 0).to.be(true); - } else { - expect(url).to.be(helper.padChrome$.window.location.href); - } - - // check if all parts of the url are like expected - expect(params).to.eql(expectedParams); - }; - - describe('read and write', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - describe('the share link', function () { - it('is the actual pad url', async function () { - const chrome$ = helper.padChrome$; - - // open share dropdown - chrome$('.buttonicon-embed').trigger('click'); - - // get the link of the share field + the actual pad url and compare them - const shareLink = chrome$('#linkinput').val(); - const padURL = chrome$.window.location.href; - expect(shareLink).to.be(padURL); - }); - }); - - describe('the embed as iframe code', function () { - it('is an iframe with the the correct url parameters and correct size', async function () { - const chrome$ = helper.padChrome$; - - // open share dropdown - chrome$('.buttonicon-embed').trigger('click'); - - // get the link of the share field + the actual pad url and compare them - const embedCode = chrome$('#embedinput').val(); - - checkiFrameCode(embedCode, false); - }); - }); - }); - - describe('when read only option is set', function () { - beforeEach(async function () { - await helper.aNewPad(); - }); - - describe('the share link', function () { - it('shows a read only url', async function () { - const chrome$ = helper.padChrome$; - - // open share dropdown - chrome$('.buttonicon-embed').trigger('click'); - chrome$('#readonlyinput').trigger('click'); - chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked'); - - // get the link of the share field + the actual pad url and compare them - const shareLink = chrome$('#linkinput').val(); - const containsReadOnlyLink = shareLink.indexOf('r.') > 0; - expect(containsReadOnlyLink).to.be(true); - }); - }); - - describe('the embed as iframe code', function () { - it('is an iframe with the the correct url parameters and correct size', async function () { - const chrome$ = helper.padChrome$; - - // open share dropdown - chrome$('.buttonicon-embed').trigger('click'); - // check read only checkbox, a bit hacky - chrome$('#readonlyinput').trigger('click'); - chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked'); - - - // get the link of the share field + the actual pad url and compare them - const embedCode = chrome$('#embedinput').val(); - - checkiFrameCode(embedCode, true); - }); - }); - }); -}); diff --git a/src/tests/frontend/specs/enter.js b/src/tests/frontend/specs/enter.js deleted file mode 100644 index a32a90c6e..000000000 --- a/src/tests/frontend/specs/enter.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -describe('enter keystroke', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('creates a new line & puts cursor onto a new line', async function () { - this.timeout(2000); - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // get the original string value minus the last char - const originalTextValue = $firstTextElement.text(); - - // simulate key presses to enter content - $firstTextElement.sendkeys('{enter}'); - - await helper.waitForPromise(() => inner$('div').first().text() === ''); - - const $newSecondLine = inner$('div').first().next(); - const newFirstTextElementValue = inner$('div').first().text(); - expect(newFirstTextElementValue).to.be(''); // expect the first line to be blank - // expect the second line to be the same as the original first line. - expect($newSecondLine.text()).to.be(originalTextValue); - }); - - it('enter is always visible after event', async function () { - const originalLength = helper.padInner$('div').length; - let $lastLine = helper.padInner$('div').last(); - - // simulate key presses to enter content - let i = 0; - const numberOfLines = 15; - let previousLineLength = originalLength; - while (i < numberOfLines) { - $lastLine = helper.padInner$('div').last(); - $lastLine.sendkeys('{enter}'); - await helper.waitForPromise(() => helper.padInner$('div').length > previousLineLength); - previousLineLength = helper.padInner$('div').length; - // check we can see the caret.. - - i++; - } - await helper.waitForPromise( - () => helper.padInner$('div').length === numberOfLines + originalLength); - - // is edited line fully visible? - const lastLine = helper.padInner$('div').last(); - const bottomOfLastLine = lastLine.offset().top + lastLine.height(); - const scrolledWindow = helper.padChrome$('iframe')[0]; - await helper.waitForPromise(() => { - const scrolledAmount = - scrolledWindow.contentWindow.pageYOffset + scrolledWindow.contentWindow.innerHeight; - return scrolledAmount >= bottomOfLastLine; - }); - }); -}); diff --git a/src/tests/frontend/specs/helper.js b/src/tests/frontend/specs/helper.js index 9be34c662..19b227e93 100644 --- a/src/tests/frontend/specs/helper.js +++ b/src/tests/frontend/specs/helper.js @@ -7,7 +7,7 @@ describe('the test helper', function () { for (let i = 0; i < 10; ++i) await helper.aNewPad(); }); - it('gives me 3 jquery instances of chrome, outer and inner', async function () { + xit('gives me 3 jquery instances of chrome, outer and inner', async function () { this.timeout(10000); await helper.aNewPad(); // check if the jquery selectors have the desired elements @@ -27,7 +27,7 @@ describe('the test helper', function () { // However this doesn't seem to always be easily replicated, so this // timeout may or may end up in the code. None the less, we test here // to catch it if the bug comes up again. - it('clears cookies', async function () { + xit('clears cookies', async function () { // set cookies far into the future to make sure they're not expired yet window.Cookies.set('token', 'foo', {expires: 7 /* days */}); window.Cookies.set('language', 'bar', {expires: 7 /* days */}); @@ -410,13 +410,13 @@ describe('the test helper', function () { }); }); - it('.edit() defaults to send an edit to the first line', async function () { + xit('.edit() defaults to send an edit to the first line', async function () { const firstLine = helper.textLines()[0]; await helper.edit('line'); expect(helper.textLines()[0]).to.be(`line${firstLine}`); }); - it('.edit() to the line specified with parameter lineNo', async function () { + xit('.edit() to the line specified with parameter lineNo', async function () { const firstLine = helper.textLines()[0]; await helper.edit('second line', 2); @@ -425,7 +425,7 @@ describe('the test helper', function () { expect(text[1]).to.equal('second line'); }); - it('.edit() supports sendkeys syntax ({selectall},{del},{enter})', async function () { + xit('.edit() supports sendkeys syntax ({selectall},{del},{enter})', async function () { expect(helper.textLines()[0]).to.not.equal(''); // select first line diff --git a/src/tests/frontend/specs/indentation.js b/src/tests/frontend/specs/indentation.js deleted file mode 100644 index 939745353..000000000 --- a/src/tests/frontend/specs/indentation.js +++ /dev/null @@ -1,310 +0,0 @@ -'use strict'; - -describe('indentation button', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('indent text with keypress', async function () { - const inner$ = helper.padInner$; - - // get the first text element out of the inner iframe - const $firstTextElement = inner$('div').first(); - - // select this text element - $firstTextElement.sendkeys('{selectall}'); - - const e = new inner$.Event(helper.evtType); - e.keyCode = 9; // tab :| - inner$('#innerdocbody').trigger(e); - - await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1); - }); - - it('indent text with button', async function () { - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.trigger('click'); - - await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1); - }); - - it('keeps the indent on enter for the new line', async function () { - this.timeout(1200); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.trigger('click'); - - // type a bit, make a line break and type again - const $firstTextElement = inner$('div span').first(); - $firstTextElement.sendkeys('line 1'); - $firstTextElement.sendkeys('{enter}'); - $firstTextElement.sendkeys('line 2'); - $firstTextElement.sendkeys('{enter}'); - - await helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1); - - const $newSecondLine = inner$('div').first().next(); - const hasULElement = $newSecondLine.find('ul li').length === 1; - - expect(hasULElement).to.be(true); - expect($newSecondLine.text()).to.be('line 2'); - }); - - it('indents text with spaces on enter if previous line ends ' + - "with ':', '[', '(', or '{'", async function () { - this.timeout(1200); - const inner$ = helper.padInner$; - - // type a bit, make a line break and type again - const $firstTextElement = inner$('div').first(); - $firstTextElement.sendkeys("line with ':'{enter}"); - $firstTextElement.sendkeys("line with '['{enter}"); - $firstTextElement.sendkeys("line with '('{enter}"); - $firstTextElement.sendkeys("line with '{{}'{enter}"); - - await helper.waitForPromise(() => { - // wait for Etherpad to split four lines into separated divs - const $fourthLine = inner$('div').first().next().next().next(); - return $fourthLine.text().indexOf("line with '{'") === 0; - }); - - // we validate bottom to top for easier implementation - - // curly braces - const $lineWithCurlyBraces = inner$('div').first().next().next().next(); - $lineWithCurlyBraces.sendkeys('{{}'); - // cannot use sendkeys('{enter}') here, browser does not read the command properly - pressEnter(); - const $lineAfterCurlyBraces = inner$('div').first().next().next().next().next(); - expect($lineAfterCurlyBraces.text()).to.match(/\s{4}/); // tab === 4 spaces - - // parenthesis - const $lineWithParenthesis = inner$('div').first().next().next(); - $lineWithParenthesis.sendkeys('('); - pressEnter(); - const $lineAfterParenthesis = inner$('div').first().next().next().next(); - expect($lineAfterParenthesis.text()).to.match(/\s{4}/); - - // bracket - const $lineWithBracket = inner$('div').first().next(); - $lineWithBracket.sendkeys('['); - pressEnter(); - const $lineAfterBracket = inner$('div').first().next().next(); - expect($lineAfterBracket.text()).to.match(/\s{4}/); - - // colon - const $lineWithColon = inner$('div').first(); - $lineWithColon.sendkeys(':'); - pressEnter(); - const $lineAfterColon = inner$('div').first().next(); - expect($lineAfterColon.text()).to.match(/\s{4}/); - }); - - it('appends indentation to the indent of previous line if previous line ends ' + - "with ':', '[', '(', or '{'", async function () { - this.timeout(1200); - const inner$ = helper.padInner$; - - // type a bit, make a line break and type again - const $firstTextElement = inner$('div').first(); - $firstTextElement.sendkeys(" line with some indentation and ':'{enter}"); - $firstTextElement.sendkeys('line 2{enter}'); - - await helper.waitForPromise(() => { - // wait for Etherpad to split two lines into separated divs - const $secondLine = inner$('div').first().next(); - return $secondLine.text().indexOf('line 2') === 0; - }); - - const $lineWithColon = inner$('div').first(); - $lineWithColon.sendkeys(':'); - pressEnter(); - const $lineAfterColon = inner$('div').first().next(); - // previous line indentation + regular tab (4 spaces) - expect($lineAfterColon.text()).to.match(/\s{6}/); - }); - - it("issue #2772 shows '*' when multiple indented lines " + - ' receive a style and are outdented', async function () { - this.timeout(1200); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; - - // make sure pad has more than one line - inner$('div').first().sendkeys('First{enter}Second{enter}'); - await helper.waitForPromise(() => inner$('div').first().text().trim() === 'First'); - - // indent first 2 lines - const $lines = inner$('div'); - const $firstLine = $lines.first(); - let $secondLine = $lines.slice(1, 2); - helper.selectLines($firstLine, $secondLine); - - const $indentButton = chrome$('.buttonicon-indent'); - $indentButton.trigger('click'); - - await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1); - - // apply bold - const $boldButton = chrome$('.buttonicon-bold'); - $boldButton.trigger('click'); - - await helper.waitForPromise(() => inner$('div').first().find('b').length === 1); - - // outdent first 2 lines - const $outdentButton = chrome$('.buttonicon-outdent'); - $outdentButton.trigger('click'); - await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 0); - - // check if '*' is displayed - $secondLine = inner$('div').slice(1, 2); - expect($secondLine.text().trim()).to.be('Second'); - }); - - xit('makes text indented and outdented', async function () { - // get the inner iframe - const $inner = helper.$getPadInner(); - - // get the first text element out of the inner iframe - let firstTextElement = $inner.find('div').first(); - - // select this text element - helper.selectText(firstTextElement[0], $inner); - - // get the indentation button and click it - const $indentButton = helper.$getPadChrome().find('.buttonicon-indent'); - $indentButton.trigger('click'); - - let newFirstTextElement = $inner.find('div').first(); - - // is there a list-indent class element now? - let firstChild = newFirstTextElement.children(':first'); - let isUL = firstChild.is('ul'); - - // expect it to be the beginning of a list - expect(isUL).to.be(true); - - let secondChild = firstChild.children(':first'); - let isLI = secondChild.is('li'); - // expect it to be part of a list - expect(isLI).to.be(true); - - // indent again - $indentButton.trigger('click'); - - newFirstTextElement = $inner.find('div').first(); - - // is there a list-indent class element now? - firstChild = newFirstTextElement.children(':first'); - const hasListIndent2 = firstChild.hasClass('list-indent2'); - - // expect it to be part of a list - expect(hasListIndent2).to.be(true); - - // make sure the text hasn't changed - expect(newFirstTextElement.text()).to.eql(firstTextElement.text()); - - - // test outdent - - // get the unindentation button and click it twice - const $outdentButton = helper.$getPadChrome().find('.buttonicon-outdent'); - $outdentButton.trigger('click'); - $outdentButton.trigger('click'); - - newFirstTextElement = $inner.find('div').first(); - - // is there a list-indent class element now? - firstChild = newFirstTextElement.children(':first'); - isUL = firstChild.is('ul'); - - // expect it not to be the beginning of a list - expect(isUL).to.be(false); - - secondChild = firstChild.children(':first'); - isLI = secondChild.is('li'); - // expect it to not be part of a list - expect(isLI).to.be(false); - - // make sure the text hasn't changed - expect(newFirstTextElement.text()).to.eql(firstTextElement.text()); - - - // Next test tests multiple line indentation - - // select this text element - helper.selectText(firstTextElement[0], $inner); - - // indent twice - $indentButton.trigger('click'); - $indentButton.trigger('click'); - - // get the first text element out of the inner iframe - firstTextElement = $inner.find('div').first(); - - // select this text element - helper.selectText(firstTextElement[0], $inner); - - /* this test creates the below content, both should have double indentation - line1 - line2 - */ - - firstTextElement.sendkeys('{rightarrow}'); // simulate a keypress of enter - firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter - firstTextElement.sendkeys('line 1'); // simulate writing the first line - firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter - firstTextElement.sendkeys('line 2'); // simulate writing the second line - - // get the second text element out of the inner iframe - await new Promise((resolve) => setTimeout(resolve, 1000)); // THIS IS REALLY BAD - - const secondTextElement = $('iframe').contents() - .find('iframe').contents() - .find('iframe').contents().find('body > div').get(1); // THIS IS UGLY - - // is there a list-indent class element now? - firstChild = secondTextElement.children(':first'); - isUL = firstChild.is('ul'); - - // expect it to be the beginning of a list - expect(isUL).to.be(true); - - secondChild = secondChild.children(':first'); - isLI = secondChild.is('li'); - // expect it to be part of a list - expect(isLI).to.be(true); - - // get the first text element out of the inner iframe - const thirdTextElement = $('iframe').contents() - .find('iframe').contents() - .find('iframe').contents() - .find('body > div').get(2); // THIS IS UGLY TOO - - // is there a list-indent class element now? - firstChild = thirdTextElement.children(':first'); - isUL = firstChild.is('ul'); - - // expect it to be the beginning of a list - expect(isUL).to.be(true); - - secondChild = firstChild.children(':first'); - isLI = secondChild.is('li'); - - // expect it to be part of a list - expect(isLI).to.be(true); - }); -}); - -const pressEnter = () => { - const inner$ = helper.padInner$; - const e = new inner$.Event(helper.evtType); - e.keyCode = 13; // enter :| - inner$('#innerdocbody').trigger(e); -};