mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13:34 +01:00
Added playwright tests. (#6212)
* Added playwright tests. * Added clear authorship color. * Ported enter ts. * Ported more tests. * Commented helper tests. * Fixed admin tests. * Fixed. * Fixed admin pages not there. * Fixed waiting. * Upload playwright report. * Remove saucelabs * Fixed waiting. * Fixed upload artifact. * Also install deps. * Added retry mechanism. * Added timeout for restart etherpad server. * Fixed tests. * Added frontend playwright tests.
This commit is contained in:
parent
db46ffb63b
commit
c2699e4528
40 changed files with 1568 additions and 1285 deletions
94
.github/workflows/frontend-admin-tests.yml
vendored
94
.github/workflows/frontend-admin-tests.yml
vendored
|
@ -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
|
||||
|
|
77
.github/workflows/frontend-tests.yml
vendored
77
.github/workflows/frontend-tests.yml
vendored
|
@ -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
|
||||
|
||||
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -25,3 +25,6 @@ out/
|
|||
plugin_packages
|
||||
pnpm-lock.yaml
|
||||
/src/templates/admin
|
||||
/src/test-results
|
||||
playwright-report
|
||||
state.json
|
||||
|
|
|
@ -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<PluginDef[]>([])
|
||||
const [installedPlugins, setInstalledPlugins] = useState<InstalledPlugin[]>([])
|
||||
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<SearchParams>({
|
||||
offset: 0,
|
||||
limit: 99999,
|
||||
|
@ -125,7 +137,7 @@ export const HomePage = () => {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody style={{overflow: 'auto'}}>
|
||||
{installedPlugins.map((plugin, index) => {
|
||||
{sortedInstalledPlugins.map((plugin, index) => {
|
||||
return <tr key={index}>
|
||||
<td>{plugin.name}</td>
|
||||
<td>{plugin.version}</td>
|
||||
|
|
|
@ -33,9 +33,9 @@ export const LoginScreen = ()=>{
|
|||
<h1 className="login-title">Login Etherpad</h1>
|
||||
<div className="login-inner-box">
|
||||
<div>Username</div>
|
||||
<input className="login-textinput" type="text" value={username} onChange={v => setUsername(v.target.value)} placeholder="Username"/>
|
||||
<input className="login-textinput" type="text" name="username" value={username} onChange={v => setUsername(v.target.value)} placeholder="Username"/>
|
||||
<div>Passwort</div>
|
||||
<input className="login-textinput" type="password" value={password}
|
||||
<input className="login-textinput" type="password" name="password" value={password}
|
||||
onChange={v => setPassword(v.target.value)} placeholder="Password"/>
|
||||
<input type="button" value="Login" onClick={login} className="login-button"/>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@ export const LoadingScreen = ()=>{
|
|||
const showLoading = useStore(state => state.showLoading)
|
||||
|
||||
return <Dialog.Root open={showLoading}><Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
|
||||
<Dialog.Overlay className="loading-screen fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
|
||||
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 dialog-content">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="animate-spin w-16 h-16 border-t-2 border-b-2 border-[--fg-color] rounded-full"></div>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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"
|
||||
|
|
69
src/playwright.config.ts
Normal file
69
src/playwright.config.ts
Normal file
|
@ -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,
|
||||
// },
|
||||
});
|
59
src/tests/frontend-new/admin-spec/adminsettings.spec.ts
Normal file
59
src/tests/frontend-new/admin-spec/adminsettings.spec.ts
Normal file
|
@ -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')
|
||||
});
|
||||
})
|
|
@ -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');
|
||||
});
|
||||
|
111
src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts
Normal file
111
src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts
Normal file
|
@ -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.
|
||||
});
|
||||
*/
|
32
src/tests/frontend-new/helper/adminhelper.ts
Normal file
32
src/tests/frontend-new/helper/adminhelper.ts
Normal file
|
@ -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')
|
||||
}
|
155
src/tests/frontend-new/helper/padHelper.ts
Normal file
155
src/tests/frontend-new/helper/padHelper.ts
Normal file
|
@ -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<Frame> => {
|
||||
return page.frame('ace_outer')!;
|
||||
}
|
||||
|
||||
export const getPadBody = async (page: Page): Promise<Locator> => {
|
||||
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<boolean> => {
|
||||
const classes = await page.locator('#chatbox').getAttribute('class')
|
||||
return classes !==null && classes.includes('visible')
|
||||
}
|
||||
|
||||
export const isChatBoxSticky = async (page: Page):Promise<boolean> => {
|
||||
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<string>) => {
|
||||
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()
|
||||
}
|
35
src/tests/frontend-new/helper/settingsHelper.ts
Normal file
35
src/tests/frontend-new/helper/settingsHelper.ts
Normal file
|
@ -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)')
|
||||
}
|
27
src/tests/frontend-new/specs/alphabet.spec.ts
Normal file
27
src/tests/frontend-new/specs/alphabet.spec.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
50
src/tests/frontend-new/specs/bold.spec.ts
Normal file
50
src/tests/frontend-new/specs/bold.spec.ts
Normal file
|
@ -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');
|
||||
})
|
||||
|
||||
})
|
35
src/tests/frontend-new/specs/change_user_name.spec.ts
Normal file
35
src/tests/frontend-new/specs/change_user_name.spec.ts
Normal file
|
@ -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)
|
||||
});
|
116
src/tests/frontend-new/specs/chat.spec.ts
Normal file
116
src/tests/frontend-new/specs/chat.spec.ts
Normal file
|
@ -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)
|
||||
});
|
87
src/tests/frontend-new/specs/clear_authorship_color.spec.ts
Normal file
87
src/tests/frontend-new/specs/clear_authorship_color.spec.ts
Normal file
|
@ -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)
|
||||
});
|
94
src/tests/frontend-new/specs/collab_client.spec.ts
Normal file
94
src/tests/frontend-new/specs/collab_client.spec.ts
Normal file
|
@ -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<expectedLines.length;i++){
|
||||
expect(await body1.locator('div').nth(i).textContent()).toBe(expectedLines[i]);
|
||||
}
|
||||
|
||||
for (let i=0;i<expectedLines.length;i++){
|
||||
expect(await body2.locator('div').nth(i).textContent()).toBe(expectedLines[i]);
|
||||
}
|
||||
});
|
||||
});
|
22
src/tests/frontend-new/specs/delete.spec.ts
Normal file
22
src/tests/frontend-new/specs/delete.spec.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
// 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));
|
||||
})
|
146
src/tests/frontend-new/specs/embed_value.spec.ts
Normal file
146
src/tests/frontend-new/specs/embed_value.spec.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
})
|
63
src/tests/frontend-new/specs/enter.spec.ts
Normal file
63
src/tests/frontend-new/specs/enter.spec.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
241
src/tests/frontend-new/specs/indentation.spec.ts
Normal file
241
src/tests/frontend-new/specs/indentation.spec.ts
Normal file
|
@ -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());
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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...');
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 <b> 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 <b> 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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
||||
// <p data-authorid="a.qjkwNs4z0pPROphS"
|
||||
// class="author-a-qjkwz78zs4z122z0pz80zz82zz79zphz83z">
|
||||
// <b>unnamed:</b>
|
||||
// <span class="time author-a-qjkwz78zs4z122z0pz80zz82zz79zphz83z">12:38
|
||||
// </span> JohnMcLear
|
||||
// </p>
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
};
|
Loading…
Reference in a new issue