mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-31 19:02:59 +01:00
Ported more tests to playwright. (#6214)
* Ported more tests to playwright. * Added language test. * Fixed failing tests. Moved another test. * Removed frontend tests. * Splitted runners. * Fixed runners. * Split up into the different browser environments. * Added github reporter. * Added change user color test.
This commit is contained in:
parent
19ee8c2afa
commit
078324c0d1
35 changed files with 1702 additions and 1229 deletions
238
.github/workflows/frontend-tests.yml
vendored
238
.github/workflows/frontend-tests.yml
vendored
|
@ -7,11 +7,9 @@ permissions:
|
|||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
withoutplugins:
|
||||
name: without plugins
|
||||
playwright-chrome:
|
||||
name: Playwright Chrome
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Generate Sauce Labs strings
|
||||
|
@ -37,6 +35,7 @@ jobs:
|
|||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
if: always()
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
@ -53,79 +52,7 @@ jobs:
|
|||
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
|
||||
-
|
||||
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 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/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
|
||||
run: cp ./src/tests/settings.json settings.json
|
||||
- name: Run the frontend tests
|
||||
shell: bash
|
||||
run: |
|
||||
|
@ -141,20 +68,80 @@ jobs:
|
|||
sleep 1
|
||||
done
|
||||
cd src
|
||||
pnpm exec playwright install
|
||||
pnpm exec playwright install-deps
|
||||
pnpm run test-ui
|
||||
pnpm exec playwright install chromium --with-deps
|
||||
pnpm run test-ui --project=chromium
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-${{ matrix.node }}
|
||||
name: playwright-report-${{ matrix.node }}-chrome
|
||||
path: src/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
withplugins:
|
||||
name: with plugins
|
||||
playwright-firefox:
|
||||
name: Playwright Firefox
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
if: always()
|
||||
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 ./src/tests/settings.json 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 firefox --with-deps
|
||||
pnpm run test-ui --project=firefox
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-${{ matrix.node }}-firefox
|
||||
path: src/playwright-report/
|
||||
retention-days: 30
|
||||
playwright-webkit:
|
||||
name: Playwright Webkit
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
|
||||
steps:
|
||||
-
|
||||
|
@ -169,7 +156,7 @@ jobs:
|
|||
-
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 21
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
|
@ -181,40 +168,14 @@ jobs:
|
|||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
if: always()
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
-
|
||||
name: Install Etherpad plugins
|
||||
# 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
|
||||
ep_author_hover
|
||||
ep_cursortrace
|
||||
ep_embedmedia
|
||||
ep_font_size
|
||||
ep_hash_auth
|
||||
ep_headings2
|
||||
ep_image_upload
|
||||
ep_markdown
|
||||
ep_readonly_guest
|
||||
ep_set_title_on_pad
|
||||
ep_spellcheck
|
||||
ep_subscript_and_superscript
|
||||
ep_table_of_contents
|
||||
# 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
|
||||
# in the top-level node_modules. As of v6.20.10, npm's hoist logic appears
|
||||
# to be buggy, because it sometimes removes dependencies from
|
||||
# src/node_modules but fails to add them to the top-level node_modules.
|
||||
# Even if npm correctly hoists the dependencies, the hoisting seems to
|
||||
# confuse tools such as `npm outdated`, `npm update`, and some ESLint
|
||||
# rules.
|
||||
- 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
|
||||
|
@ -224,31 +185,30 @@ jobs:
|
|||
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": 1000000/' -i settings.json
|
||||
# XXX we should probably run all tests, because plugins could effect their results
|
||||
-
|
||||
name: Remove standard frontend test files, so only plugin tests are run
|
||||
run: rm 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 tests
|
||||
run: cp ./src/tests/settings.json settings.json
|
||||
- name: Run the frontend 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/runner.sh
|
||||
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 webkit --with-deps
|
||||
pnpm run test-ui --project=webkit
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-${{ matrix.node }}-webkit
|
||||
path: src/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -96,7 +96,6 @@
|
|||
"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",
|
||||
"split-grid": "^1.0.11",
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import {defineConfig, devices, test} from '@playwright/test';
|
||||
|
||||
|
||||
export const defaultExpectTimeout = process.env.CI ? 20 * 1000 : 5000
|
||||
export const defaultTestTimeout = 90 * 1000
|
||||
|
||||
/**
|
||||
* 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,
|
||||
reporter: process.env.CI ? 'github' : 'html',
|
||||
expect: { timeout: defaultExpectTimeout },
|
||||
timeout: defaultTestTimeout,
|
||||
retries: 2,
|
||||
workers: 20,
|
||||
/* 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('/')`. */
|
||||
|
@ -20,6 +25,7 @@ export default defineConfig({
|
|||
baseURL: "localhost:9001",
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
video: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
|
@ -33,7 +39,11 @@ export default defineConfig({
|
|||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'chrome-firefox',
|
||||
use:
|
||||
{...devices['Desktop Firefox'], ...devices['Desktop Chrome']},
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Frame, Locator, Page} from "@playwright/test";
|
||||
import {MapArrayType} from "../../../node/types/MapType";
|
||||
import {randomInt} from "node:crypto";
|
||||
import {randomUUID} from "node:crypto";
|
||||
|
||||
export const getPadOuter = async (page: Page): Promise<Frame> => {
|
||||
return page.frame('ace_outer')!;
|
||||
|
@ -115,7 +115,7 @@ export const appendQueryParams = async (page: Page, queryParameters: MapArrayTyp
|
|||
|
||||
export const goToNewPad = async (page: Page) => {
|
||||
// create a new pad before each test run
|
||||
const padId = "FRONTEND_TESTS"+randomInt(0, 1000);
|
||||
const padId = "FRONTEND_TESTS"+randomUUID();
|
||||
await page.goto('http://localhost:9001/p/'+padId);
|
||||
await page.waitForSelector('iframe[name="ace_outer"]');
|
||||
return padId;
|
||||
|
@ -128,6 +128,8 @@ export const goToPad = async (page: Page, padId: string) => {
|
|||
|
||||
|
||||
export const clearPadContent = async (page: Page) => {
|
||||
const body = await getPadBody(page);
|
||||
await body.click();
|
||||
await page.keyboard.down('Control');
|
||||
await page.keyboard.press('A');
|
||||
await page.keyboard.up('Control');
|
||||
|
|
20
src/tests/frontend-new/helper/timeslider.ts
Normal file
20
src/tests/frontend-new/helper/timeslider.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {Page} from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Sets the src-attribute of the main iframe to the timeslider
|
||||
* In case a revision is given, sets the timeslider to this specific revision.
|
||||
* Defaults to going to the last revision.
|
||||
* It waits until the timer is filled with date and time, because it's one of the
|
||||
* last things that happen during timeslider load
|
||||
*
|
||||
* @param page
|
||||
* @param {number} [revision] the optional revision
|
||||
* @returns {Promise}
|
||||
* @todo for some reason this does only work the first time, you cannot
|
||||
* goto rev 0 and then via the same method to rev 5. Use buttons instead
|
||||
*/
|
||||
export const gotoTimeslider = async (page: Page, revision: number): Promise<any> => {
|
||||
let revisionString = Number.isInteger(revision) ? `#${revision}` : '';
|
||||
await page.goto(`${page.url()}/timeslider${revisionString}`);
|
||||
await page.waitForSelector('#timer')
|
||||
};
|
103
src/tests/frontend-new/specs/change_user_color.spec.ts
Normal file
103
src/tests/frontend-new/specs/change_user_color.spec.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {goToNewPad, sendChatMessage, showChat} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({page}) => {
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
test.describe('change user color', function () {
|
||||
|
||||
test('Color picker matches original color and remembers the user color after a refresh',
|
||||
async function ({page}) {
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
let $userButton = page.locator('.buttonicon-showusers');
|
||||
await $userButton.click()
|
||||
|
||||
let $userSwatch = page.locator('#myswatch');
|
||||
await $userSwatch.click()
|
||||
// Change the color value of the Farbtastic color picker
|
||||
|
||||
const $colorPickerSave = page.locator('#mycolorpickersave');
|
||||
let $colorPickerPreview = page.locator('#mycolorpickerpreview');
|
||||
|
||||
// Same color represented in two different ways
|
||||
const testColorHash = '#abcdef';
|
||||
const testColorRGB = 'rgb(171, 205, 239)';
|
||||
|
||||
// Check that the color picker matches the automatically assigned random color on the swatch.
|
||||
// NOTE: This has a tiny chance of creating a false positive for passing in the
|
||||
// off-chance the randomly assigned color is the same as the test color.
|
||||
expect(await $colorPickerPreview.getAttribute('style')).toContain(await $userSwatch.getAttribute('style'));
|
||||
|
||||
// The swatch updates as the test color is picked.
|
||||
await page.evaluate((testRGBColor) => {
|
||||
document.getElementById('mycolorpickerpreview')!.style.backgroundColor = testRGBColor;
|
||||
}, testColorRGB
|
||||
)
|
||||
|
||||
await $colorPickerSave.click();
|
||||
|
||||
// give it a second to save the color on the server side
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
|
||||
// get a new pad, but don't clear the cookies
|
||||
await goToNewPad(page)
|
||||
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
await $userButton.click()
|
||||
|
||||
await $userSwatch.click()
|
||||
|
||||
|
||||
|
||||
expect(await $colorPickerPreview.getAttribute('style')).toContain(await $userSwatch.getAttribute('style'));
|
||||
});
|
||||
|
||||
test('Own user color is shown when you enter a chat', async function ({page}) {
|
||||
|
||||
const colorOption = page.locator('#options-colorscheck');
|
||||
if (!(await colorOption.isChecked())) {
|
||||
await colorOption.check();
|
||||
}
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
const $userButton = page.locator('.buttonicon-showusers');
|
||||
await $userButton.click()
|
||||
|
||||
const $userSwatch = page.locator('#myswatch');
|
||||
await $userSwatch.click()
|
||||
|
||||
const $colorPickerSave = page.locator('#mycolorpickersave');
|
||||
|
||||
// Same color represented in two different ways
|
||||
const testColorHash = '#abcdef';
|
||||
const testColorRGB = 'rgb(171, 205, 239)';
|
||||
|
||||
// The swatch updates as the test color is picked.
|
||||
await page.evaluate((testRGBColor) => {
|
||||
document.getElementById('mycolorpickerpreview')!.style.backgroundColor = testRGBColor;
|
||||
}, testColorRGB
|
||||
)
|
||||
|
||||
|
||||
await $colorPickerSave.click();
|
||||
// click on the chat button to make chat visible
|
||||
await showChat(page)
|
||||
await sendChatMessage(page, 'O hi');
|
||||
|
||||
// wait until the chat message shows up
|
||||
const chatP = page.locator('#chattext').locator('p')
|
||||
const chatText = await chatP.innerText();
|
||||
|
||||
expect(chatText).toContain('O hi');
|
||||
|
||||
const color = await chatP.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-color');
|
||||
}, chatText);
|
||||
|
||||
expect(color).toBe(testColorRGB);
|
||||
});
|
||||
});
|
|
@ -63,8 +63,7 @@ test.describe('embed links', function () {
|
|||
// 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}) {
|
||||
test('the share link is the actual pad url', async function ({page}) {
|
||||
|
||||
const shareButton = page.locator('.buttonicon-embed')
|
||||
// open share dropdown
|
||||
|
@ -75,9 +74,7 @@ test.describe('embed links', function () {
|
|||
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')
|
||||
|
@ -90,7 +87,6 @@ test.describe('embed links', function () {
|
|||
await checkiFrameCode(embedCode, false, page);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('when read only option is set', function () {
|
||||
test.beforeEach(async ({ page })=>{
|
||||
|
@ -98,8 +94,7 @@ test.describe('embed links', function () {
|
|||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
test.describe('the share link', function () {
|
||||
test('shows a read only url', async function ({page}) {
|
||||
test('the share link shows a read only url', async function ({page}) {
|
||||
|
||||
// open share dropdown
|
||||
const shareButton = page.locator('.buttonicon-embed')
|
||||
|
@ -115,10 +110,8 @@ test.describe('embed links', function () {
|
|||
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}) {
|
||||
test('the embed as iframe code is an iframe with the the correct url parameters and correct size', async function ({page}) {
|
||||
|
||||
|
||||
// open share dropdown
|
||||
|
@ -139,8 +132,5 @@ test.describe('embed links', function () {
|
|||
|
||||
await checkiFrameCode(embedCode, true, page);
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
|
39
src/tests/frontend-new/specs/font_type.spec.ts
Normal file
39
src/tests/frontend-new/specs/font_type.spec.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {getPadBody, goToNewPad} from "../helper/padHelper";
|
||||
import {showSettings} from "../helper/settingsHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
// create a new pad before each test run
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
|
||||
test.describe('font select', function () {
|
||||
// create a new pad before each test run
|
||||
|
||||
test('makes text RobotoMono', async function ({page}) {
|
||||
// click on the settings button to make settings visible
|
||||
await showSettings(page);
|
||||
|
||||
// get the font menu and RobotoMono option
|
||||
const viewFontMenu = page.locator('#viewfontmenu');
|
||||
|
||||
// select RobotoMono and fire change event
|
||||
// $RobotoMonooption.attr('selected','selected');
|
||||
// commenting out above will break safari test
|
||||
const dropdown = page.locator('.dropdowns-container .dropdown-line .current').nth(0)
|
||||
await dropdown.click()
|
||||
await page.locator('li:text("RobotoMono")').click()
|
||||
|
||||
await viewFontMenu.dispatchEvent('change');
|
||||
const padBody = await getPadBody(page)
|
||||
const color = await padBody.evaluate((e) => {
|
||||
return window.getComputedStyle(e).getPropertyValue("font-family")
|
||||
})
|
||||
|
||||
|
||||
// check if font changed to RobotoMono
|
||||
const containsStr = color.toLowerCase().indexOf('robotomono');
|
||||
expect(containsStr).not.toBe(-1);
|
||||
});
|
||||
});
|
|
@ -25,7 +25,7 @@ test.describe('indentation button', function () {
|
|||
const padBody = await getPadBody(page);
|
||||
await page.locator('.buttonicon-indent').click()
|
||||
|
||||
const uls = padBody.locator('div').first().locator('ul li')
|
||||
const uls = padBody.locator('div').first().locator('ul')
|
||||
await expect(uls).toHaveCount(1);
|
||||
});
|
||||
|
||||
|
|
56
src/tests/frontend-new/specs/inner_height.spec.ts
Normal file
56
src/tests/frontend-new/specs/inner_height.spec.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
test.describe('height regression after ace.js refactoring', function () {
|
||||
|
||||
test('clientHeight should equal scrollHeight with few lines', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
|
||||
const iframe = page.locator('iframe').first()
|
||||
const scrollHeight = await iframe.evaluate((element) => {
|
||||
return element.scrollHeight;
|
||||
})
|
||||
|
||||
const clientHeight = await iframe.evaluate((element) => {
|
||||
return element.clientHeight;
|
||||
})
|
||||
|
||||
|
||||
expect(clientHeight).toEqual(scrollHeight);
|
||||
});
|
||||
|
||||
test('client height should be less than scrollHeight with many lines', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
|
||||
await writeToPad(page,'Test line\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
|
||||
|
||||
const iframe = page.locator('iframe').first()
|
||||
const scrollHeight = await iframe.evaluate((element) => {
|
||||
return element.scrollHeight;
|
||||
})
|
||||
|
||||
const clientHeight = await iframe.evaluate((element) => {
|
||||
return element.clientHeight;
|
||||
})
|
||||
|
||||
// Need to poll because the heights take some time to settle.
|
||||
expect(clientHeight).toBeLessThanOrEqual(scrollHeight);
|
||||
});
|
||||
});
|
65
src/tests/frontend-new/specs/italic.spec.ts
Normal file
65
src/tests/frontend-new/specs/italic.spec.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
test.describe('italic some text', function () {
|
||||
|
||||
test('makes text italic using button', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
|
||||
// get the first text element out of the inner iframe
|
||||
const $firstTextElement = padBody.locator('div').first();
|
||||
await $firstTextElement.click()
|
||||
await writeToPad(page, 'Foo')
|
||||
|
||||
// select this text element
|
||||
await padBody.click()
|
||||
await page.keyboard.press('Control+A');
|
||||
|
||||
// get the bold button and click it
|
||||
const $boldButton = page.locator('.buttonicon-italic');
|
||||
await $boldButton.click();
|
||||
|
||||
// ace creates a new dom element when you press a button, just get the first text element again
|
||||
const $newFirstTextElement = padBody.locator('div').first();
|
||||
|
||||
// is there a <i> element now?
|
||||
// expect it to be italic
|
||||
await expect($newFirstTextElement.locator('i')).toHaveCount(1);
|
||||
|
||||
|
||||
// make sure the text hasn't changed
|
||||
expect(await $newFirstTextElement.textContent()).toEqual(await $firstTextElement.textContent());
|
||||
});
|
||||
|
||||
test('makes text italic using keypress', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
|
||||
// get the first text element out of the inner iframe
|
||||
const $firstTextElement = padBody.locator('div').first();
|
||||
|
||||
// select this text element
|
||||
await writeToPad(page, 'Foo')
|
||||
|
||||
await page.keyboard.press('Control+A');
|
||||
|
||||
await page.keyboard.press('Control+I');
|
||||
|
||||
// ace creates a new dom element when you press a button, just get the first text element again
|
||||
const $newFirstTextElement = padBody.locator('div').first();
|
||||
|
||||
// is there a <i> element now?
|
||||
// expect it to be italic
|
||||
await expect($newFirstTextElement.locator('i')).toHaveCount(1);
|
||||
|
||||
// make sure the text hasn't changed
|
||||
expect(await $newFirstTextElement.textContent()).toBe(await $firstTextElement.textContent());
|
||||
});
|
||||
});
|
88
src/tests/frontend-new/specs/language.spec.ts
Normal file
88
src/tests/frontend-new/specs/language.spec.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {getPadBody, goToNewPad} from "../helper/padHelper";
|
||||
import {showSettings} from "../helper/settingsHelper";
|
||||
|
||||
test.beforeEach(async ({ page, browser })=>{
|
||||
const context = await browser.newContext()
|
||||
await context.clearCookies()
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
|
||||
|
||||
test.describe('Language select and change', function () {
|
||||
|
||||
// Destroy language cookies
|
||||
test('makes text german', async function ({page}) {
|
||||
// click on the settings button to make settings visible
|
||||
await showSettings(page)
|
||||
|
||||
// click the language button
|
||||
const languageDropDown = page.locator('.nice-select').nth(1)
|
||||
|
||||
await languageDropDown.click()
|
||||
await page.locator('.nice-select').locator('[data-value=de]').click()
|
||||
await expect(languageDropDown.locator('.current')).toHaveText('Deutsch')
|
||||
|
||||
// select german
|
||||
await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)');
|
||||
});
|
||||
|
||||
test('makes text English', async function ({page}) {
|
||||
|
||||
await showSettings(page)
|
||||
|
||||
// click the language button
|
||||
await page.locator('.nice-select').nth(1).locator('.current').click()
|
||||
await page.locator('.nice-select').locator('[data-value=de]').click()
|
||||
|
||||
// select german
|
||||
await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)');
|
||||
|
||||
|
||||
// change to english
|
||||
await page.locator('.nice-select').nth(1).locator('.current').click()
|
||||
await page.locator('.nice-select').locator('[data-value=en]').click()
|
||||
|
||||
// check if the language is now English
|
||||
await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title !== 'Fett (Strg-B)');
|
||||
});
|
||||
|
||||
test('changes direction when picking an rtl lang', async function ({page}) {
|
||||
|
||||
await showSettings(page)
|
||||
|
||||
// click the language button
|
||||
await page.locator('.nice-select').nth(1).locator('.current').click()
|
||||
await page.locator('.nice-select').locator('[data-value=de]').click()
|
||||
|
||||
// select german
|
||||
await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)');
|
||||
|
||||
// click the language button
|
||||
await page.locator('.nice-select').nth(1).locator('.current').click()
|
||||
// select arabic
|
||||
// $languageoption.attr('selected','selected'); // Breaks the test..
|
||||
await page.locator('.nice-select').locator('[data-value=ar]').click()
|
||||
|
||||
await page.waitForSelector('html[dir="rtl"]')
|
||||
});
|
||||
|
||||
test('changes direction when picking an ltr lang', async function ({page}) {
|
||||
await showSettings(page)
|
||||
|
||||
// change to english
|
||||
const languageDropDown = page.locator('.nice-select').nth(1)
|
||||
await languageDropDown.locator('.current').click()
|
||||
await languageDropDown.locator('[data-value=en]').click()
|
||||
|
||||
await expect(languageDropDown.locator('.current')).toHaveText('English')
|
||||
|
||||
// check if the language is now English
|
||||
await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title !== 'Fett (Strg-B)');
|
||||
|
||||
|
||||
await page.waitForSelector('html[dir="ltr"]')
|
||||
|
||||
});
|
||||
});
|
109
src/tests/frontend-new/specs/ordered_list.spec.ts
Normal file
109
src/tests/frontend-new/specs/ordered_list.spec.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
|
||||
test.describe('ordered_list.js', function () {
|
||||
|
||||
test('issue #4748 keeps numbers increment on OL', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await clearPadContent(page)
|
||||
await writeToPad(page, 'Line 1')
|
||||
await page.keyboard.press('Enter')
|
||||
await writeToPad(page, 'Line 2')
|
||||
|
||||
const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist')
|
||||
await padBody.locator('div').first().selectText()
|
||||
await $insertorderedlistButton.first().click();
|
||||
|
||||
const secondLine = padBody.locator('div').nth(1)
|
||||
|
||||
await secondLine.selectText()
|
||||
await $insertorderedlistButton.click();
|
||||
|
||||
expect(await secondLine.locator('ol').getAttribute('start')).toEqual('2');
|
||||
});
|
||||
|
||||
test('issue #1125 keeps the numbered list on enter for the new line', async function ({page}) {
|
||||
// EMULATES PASTING INTO A PAD
|
||||
const padBody = await getPadBody(page);
|
||||
await clearPadContent(page)
|
||||
await expect(padBody.locator('div')).toHaveCount(1)
|
||||
const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist')
|
||||
await $insertorderedlistButton.click();
|
||||
|
||||
// type a bit, make a line break and type again
|
||||
const firstTextElement = padBody.locator('div').first()
|
||||
await firstTextElement.click()
|
||||
await writeToPad(page, 'line 1')
|
||||
await page.keyboard.press('Enter')
|
||||
await writeToPad(page, 'line 2')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(padBody.locator('div span').nth(1)).toHaveText('line 2');
|
||||
|
||||
const $newSecondLine = padBody.locator('div').nth(1)
|
||||
expect(await $newSecondLine.locator('ol li').count()).toEqual(1);
|
||||
await expect($newSecondLine.locator('ol li').nth(0)).toHaveText('line 2');
|
||||
const hasLineNumber = await $newSecondLine.locator('ol').getAttribute('start');
|
||||
// This doesn't work because pasting in content doesn't work
|
||||
expect(Number(hasLineNumber)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Pressing Tab in an OL increases and decreases indentation', function () {
|
||||
|
||||
test('indent and de-indent list item 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()
|
||||
|
||||
const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist')
|
||||
await $insertorderedlistButton.click()
|
||||
|
||||
await page.keyboard.press('Tab')
|
||||
|
||||
await expect(padBody.locator('div').first().locator('.list-number2')).toHaveCount(1)
|
||||
|
||||
await page.keyboard.press('Shift+Tab')
|
||||
|
||||
|
||||
await expect(padBody.locator('div').first().locator('.list-number1')).toHaveCount(1)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test.describe('Pressing indent/outdent button in an OL increases and ' +
|
||||
'decreases indentation and bullet / ol formatting', function () {
|
||||
|
||||
test('indent and de-indent list item with indent button', 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()
|
||||
|
||||
const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist')
|
||||
await $insertorderedlistButton.click()
|
||||
|
||||
const $indentButton = page.locator('.buttonicon-indent')
|
||||
await $indentButton.dblclick() // make it indented twice
|
||||
|
||||
const outdentButton = page.locator('.buttonicon-outdent')
|
||||
|
||||
await expect(padBody.locator('div').first().locator('.list-number3')).toHaveCount(1)
|
||||
|
||||
await outdentButton.click(); // make it deindented to 1
|
||||
|
||||
await expect(padBody.locator('div').first().locator('.list-number2')).toHaveCount(1)
|
||||
});
|
||||
});
|
65
src/tests/frontend-new/specs/redo.spec.ts
Normal file
65
src/tests/frontend-new/specs/redo.spec.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
|
||||
test.describe('undo button then redo button', function () {
|
||||
|
||||
|
||||
test('redo some typing with button', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const $firstTextElement = padBody.locator('div span').first();
|
||||
const originalValue = await $firstTextElement.textContent(); // get the original value
|
||||
const newString = 'Foo';
|
||||
|
||||
await $firstTextElement.focus()
|
||||
expect(await $firstTextElement.textContent()).toContain(originalValue);
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
await writeToPad(page, newString); // send line 1 to the pad
|
||||
|
||||
const modifiedValue = await $firstTextElement.textContent(); // get the modified value
|
||||
expect(modifiedValue).not.toBe(originalValue); // expect the value to change
|
||||
|
||||
// get undo and redo buttons // click the buttons
|
||||
await page.locator('.buttonicon-undo').click() // removes foo
|
||||
await page.locator('.buttonicon-redo').click() // resends foo
|
||||
|
||||
await expect($firstTextElement).toHaveText(newString);
|
||||
|
||||
const finalValue = await padBody.locator('div').first().textContent();
|
||||
expect(finalValue).toBe(modifiedValue); // expect the value to change
|
||||
});
|
||||
|
||||
test('redo some typing with keypress', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const $firstTextElement = padBody.locator('div span').first();
|
||||
const originalValue = await $firstTextElement.textContent(); // get the original value
|
||||
const newString = 'Foo';
|
||||
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
await writeToPad(page, newString); // send line 1 to the pad
|
||||
const modifiedValue = await $firstTextElement.textContent(); // get the modified value
|
||||
expect(modifiedValue).not.toBe(originalValue); // expect the value to change
|
||||
|
||||
// undo the change
|
||||
await padBody.click()
|
||||
await page.keyboard.press('Control+Z');
|
||||
|
||||
await page.keyboard.press('Control+Y'); // redo the change
|
||||
|
||||
|
||||
await expect($firstTextElement).toHaveText(newString);
|
||||
|
||||
const finalValue = await padBody.locator('div').first().textContent();
|
||||
expect(finalValue).toBe(modifiedValue); // expect the value to change
|
||||
});
|
||||
});
|
30
src/tests/frontend-new/specs/strikethrough.spec.ts
Normal file
30
src/tests/frontend-new/specs/strikethrough.spec.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
test.describe('strikethrough button', function () {
|
||||
|
||||
test('makes text strikethrough', 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()
|
||||
|
||||
// get the strikethrough button and click it
|
||||
await page.locator('.buttonicon-strikethrough').click();
|
||||
|
||||
// ace creates a new dom element when you press a button, just get the first text element again
|
||||
|
||||
// is there a <i> element now?
|
||||
await expect($firstTextElement.locator('s')).toHaveCount(1);
|
||||
|
||||
// make sure the text hasn't changed
|
||||
expect(await $firstTextElement.textContent()).toEqual(await $firstTextElement.textContent());
|
||||
});
|
||||
});
|
37
src/tests/frontend-new/specs/timeslider.spec.ts
Normal file
37
src/tests/frontend-new/specs/timeslider.spec.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
// create a new pad before each test run
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
|
||||
// deactivated, we need a nice way to get the timeslider, this is ugly
|
||||
test.describe('timeslider button takes you to the timeslider of a pad', function () {
|
||||
|
||||
test('timeslider contained in URL', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await clearPadContent(page)
|
||||
await writeToPad(page, 'Foo'); // send line 1 to the pad
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const $firstTextElement = padBody.locator('div span').first();
|
||||
const originalValue = await $firstTextElement.textContent(); // get the original value
|
||||
await $firstTextElement.click()
|
||||
await writeToPad(page, 'Testing'); // send line 1 to the pad
|
||||
|
||||
const modifiedValue = await $firstTextElement.textContent(); // get the modified value
|
||||
expect(modifiedValue).not.toBe(originalValue); // expect the value to change
|
||||
|
||||
const $timesliderButton = page.locator('.buttonicon-history');
|
||||
await $timesliderButton.click(); // So click the timeslider link
|
||||
|
||||
await page.waitForSelector('#timeslider-wrapper')
|
||||
|
||||
const iFrameURL = page.url(); // get the url
|
||||
const inTimeslider = iFrameURL.indexOf('timeslider') !== -1;
|
||||
|
||||
expect(inTimeslider).toBe(true); // expect the value to change
|
||||
});
|
||||
});
|
76
src/tests/frontend-new/specs/timeslider_follow.spec.ts
Normal file
76
src/tests/frontend-new/specs/timeslider_follow.spec.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
'use strict';
|
||||
import {expect, Page, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
import {gotoTimeslider} from "../helper/timeslider";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
|
||||
test.describe('timeslider follow', function () {
|
||||
|
||||
// TODO needs test if content is also followed, when user a makes edits
|
||||
// while user b is in the timeslider
|
||||
test("content as it's added to timeslider", async function ({page}) {
|
||||
// send 6 revisions
|
||||
const revs = 6;
|
||||
const message = 'a\n\n\n\n\n\n\n\n\n\n';
|
||||
const newLines = message.split('\n').length;
|
||||
for (let i = 0; i < revs; i++) {
|
||||
await writeToPad(page, message)
|
||||
}
|
||||
|
||||
await gotoTimeslider(page,0);
|
||||
expect(page.url()).toContain('#0');
|
||||
|
||||
const originalTop = await page.evaluate(() => {
|
||||
return window.document.querySelector('#innerdocbody')!.getBoundingClientRect().top;
|
||||
});
|
||||
|
||||
// set to follow contents as it arrives
|
||||
await page.check('#options-followContents');
|
||||
await page.click('#playpause_button_icon');
|
||||
|
||||
// wait for the scroll
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
const currentOffset = await page.evaluate(() => {
|
||||
return window.document.querySelector('#innerdocbody')!.getBoundingClientRect().top;
|
||||
});
|
||||
|
||||
expect(currentOffset).toBeLessThanOrEqual(originalTop);
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests for bug described in #4389
|
||||
* The goal is to scroll to the first line that contains a change right before
|
||||
* the change is applied.
|
||||
*/
|
||||
test('only to lines that exist in the pad view, regression test for #4389', async function ({page}) {
|
||||
const padBody = await getPadBody(page)
|
||||
await padBody.click()
|
||||
|
||||
await clearPadContent(page)
|
||||
|
||||
await writeToPad(page,'Test line\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
|
||||
await padBody.locator('div').nth(40).click();
|
||||
await writeToPad(page, 'Another test line');
|
||||
|
||||
|
||||
await gotoTimeslider(page, 200);
|
||||
|
||||
// set to follow contents as it arrives
|
||||
await page.check('#options-followContents');
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
const oldYPosition = await page.locator('#editorcontainerbox').evaluate((el) => {
|
||||
return el.scrollTop;
|
||||
})
|
||||
expect(oldYPosition).toBe(0);
|
||||
});
|
||||
});
|
56
src/tests/frontend-new/specs/undo.spec.ts
Normal file
56
src/tests/frontend-new/specs/undo.spec.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
|
||||
test.describe('undo button', function () {
|
||||
|
||||
test('undo some typing by clicking undo button', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const firstTextElement = padBody.locator('div').first()
|
||||
const originalValue = await firstTextElement.textContent(); // get the original value
|
||||
await firstTextElement.focus()
|
||||
|
||||
await writeToPad(page, 'foo'); // send line 1 to the pad
|
||||
|
||||
const modifiedValue = await firstTextElement.textContent(); // get the modified value
|
||||
expect(modifiedValue).not.toBe(originalValue); // expect the value to change
|
||||
|
||||
// get clear authorship button as a variable
|
||||
const undoButton = page.locator('.buttonicon-undo')
|
||||
await undoButton.click() // click the button
|
||||
|
||||
await expect(firstTextElement).toHaveText(originalValue!);
|
||||
});
|
||||
|
||||
test('undo some typing using a keypress', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const firstTextElement = padBody.locator('div').first()
|
||||
const originalValue = await firstTextElement.textContent(); // get the original value
|
||||
|
||||
await firstTextElement.focus()
|
||||
await writeToPad(page, 'foo'); // send line 1 to the pad
|
||||
const modifiedValue = await firstTextElement.textContent(); // get the modified value
|
||||
expect(modifiedValue).not.toBe(originalValue); // expect the value to change
|
||||
|
||||
// undo the change
|
||||
await page.keyboard.press('Control+Z');
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
await expect(firstTextElement).toHaveText(originalValue!);
|
||||
});
|
||||
});
|
127
src/tests/frontend-new/specs/unordered_list.spec.ts
Normal file
127
src/tests/frontend-new/specs/unordered_list.spec.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
// create a new pad before each test run
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
test.describe('unordered_list.js', function () {
|
||||
test.describe('assign unordered list', function () {
|
||||
test('insert unordered list text then removes by outdent', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
const originalText = await padBody.locator('div').first().textContent();
|
||||
|
||||
const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist');
|
||||
await $insertunorderedlistButton.click();
|
||||
|
||||
await expect(padBody.locator('div').first()).toHaveText(originalText!);
|
||||
await expect(padBody.locator('div ul li')).toHaveCount(1);
|
||||
|
||||
// remove indentation by bullet and ensure text string remains the same
|
||||
const $outdentButton = page.locator('.buttonicon-outdent');
|
||||
await $outdentButton.click();
|
||||
await expect(padBody.locator('div').first()).toHaveText(originalText!);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('unassign unordered list', function () {
|
||||
// create a new pad before each test run
|
||||
|
||||
|
||||
test('insert unordered list text then remove by clicking list again', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
const originalText = await padBody.locator('div').first().textContent();
|
||||
|
||||
await padBody.locator('div').first().selectText()
|
||||
const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist');
|
||||
await $insertunorderedlistButton.click();
|
||||
|
||||
await expect(padBody.locator('div').first()).toHaveText(originalText!);
|
||||
await expect(padBody.locator('div ul li')).toHaveCount(1);
|
||||
|
||||
// remove indentation by bullet and ensure text string remains the same
|
||||
await $insertunorderedlistButton.click();
|
||||
await expect(padBody.locator('div').locator('ul')).toHaveCount(0)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test.describe('keep unordered list on enter key', function () {
|
||||
|
||||
test('Keeps the unordered list on enter for the new line', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await clearPadContent(page)
|
||||
await expect(padBody.locator('div')).toHaveCount(1)
|
||||
|
||||
const $insertorderedlistButton = page.locator('.buttonicon-insertunorderedlist')
|
||||
await $insertorderedlistButton.click();
|
||||
|
||||
// type a bit, make a line break and type again
|
||||
const $firstTextElement = padBody.locator('div').first();
|
||||
await $firstTextElement.click()
|
||||
await page.keyboard.type('line 1');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('line 2');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(padBody.locator('div span')).toHaveCount(2);
|
||||
|
||||
|
||||
const $newSecondLine = padBody.locator('div').nth(1)
|
||||
await expect($newSecondLine.locator('ul')).toHaveCount(1);
|
||||
await expect($newSecondLine).toHaveText('line 2');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Pressing Tab in an UL increases and decreases indentation', function () {
|
||||
|
||||
test('indent and de-indent list item with keypress', async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await clearPadContent(page)
|
||||
|
||||
// get the first text element out of the inner iframe
|
||||
const $firstTextElement = padBody.locator('div').first();
|
||||
|
||||
// select this text element
|
||||
await $firstTextElement.selectText();
|
||||
|
||||
const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist');
|
||||
await $insertunorderedlistButton.click();
|
||||
|
||||
await padBody.locator('div').first().click();
|
||||
await page.keyboard.press('Home');
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(padBody.locator('div').first().locator('.list-bullet2')).toHaveCount(1);
|
||||
|
||||
await page.keyboard.press('Shift+Tab');
|
||||
|
||||
await expect(padBody.locator('div').first().locator('.list-bullet1')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Pressing indent/outdent button in an UL increases and decreases indentation ' +
|
||||
'and bullet / ol formatting', function () {
|
||||
|
||||
test('indent and de-indent list item with indent button', 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();
|
||||
|
||||
const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist');
|
||||
await $insertunorderedlistButton.click();
|
||||
|
||||
await page.locator('.buttonicon-indent').click();
|
||||
|
||||
await expect(padBody.locator('div').first().locator('.list-bullet2')).toHaveCount(1);
|
||||
const outdentButton = page.locator('.buttonicon-outdent');
|
||||
await outdentButton.click();
|
||||
|
||||
await expect(padBody.locator('div').first().locator('.list-bullet1')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
});
|
51
src/tests/frontend-new/specs/urls_become_clickable.spec.ts
Normal file
51
src/tests/frontend-new/specs/urls_become_clickable.spec.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import {expect, test} from "@playwright/test";
|
||||
import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
|
||||
|
||||
test.beforeEach(async ({ page })=>{
|
||||
await goToNewPad(page);
|
||||
})
|
||||
|
||||
test.describe('entering a URL makes a link', function () {
|
||||
for (const url of ['https://etherpad.org', 'www.etherpad.org', 'https://www.etherpad.org']) {
|
||||
test(url, async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await clearPadContent(page)
|
||||
const url = 'https://etherpad.org';
|
||||
await writeToPad(page, url);
|
||||
await expect(padBody.locator('div').first()).toHaveText(url);
|
||||
await expect(padBody.locator('a')).toHaveText(url);
|
||||
await expect(padBody.locator('a')).toHaveAttribute('href', url);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test.describe('special characters inside URL', async function () {
|
||||
for (const char of '-:@_.,~%+/?=&#!;()[]$\'*') {
|
||||
const url = `https://etherpad.org/${char}foo`;
|
||||
test(url, async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await clearPadContent(page)
|
||||
await padBody.click()
|
||||
await clearPadContent(page)
|
||||
await writeToPad(page, url);
|
||||
await expect(padBody.locator('div').first()).toHaveText(url);
|
||||
await expect(padBody.locator('a')).toHaveText(url);
|
||||
await expect(padBody.locator('a')).toHaveAttribute('href', url);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('punctuation after URL is ignored', ()=> {
|
||||
for (const char of ':.,;?!)]\'*') {
|
||||
const want = 'https://etherpad.org';
|
||||
const input = want + char;
|
||||
test(input, async function ({page}) {
|
||||
const padBody = await getPadBody(page);
|
||||
await clearPadContent(page)
|
||||
await writeToPad(page, input);
|
||||
await expect(padBody.locator('a')).toHaveCount(1);
|
||||
await expect(padBody.locator('a')).toHaveAttribute('href', want);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,102 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('change user color', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('Color picker matches original color and remembers the user color' +
|
||||
' after a refresh', async function () {
|
||||
this.timeout(10000);
|
||||
let chrome$ = helper.padChrome$;
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
let $userButton = chrome$('.buttonicon-showusers');
|
||||
$userButton.trigger('click');
|
||||
|
||||
let $userSwatch = chrome$('#myswatch');
|
||||
$userSwatch.trigger('click');
|
||||
|
||||
const fb = chrome$.farbtastic('#colorpicker');
|
||||
const $colorPickerSave = chrome$('#mycolorpickersave');
|
||||
let $colorPickerPreview = chrome$('#mycolorpickerpreview');
|
||||
|
||||
// Same color represented in two different ways
|
||||
const testColorHash = '#abcdef';
|
||||
const testColorRGB = 'rgb(171, 205, 239)';
|
||||
|
||||
// Check that the color picker matches the automatically assigned random color on the swatch.
|
||||
// NOTE: This has a tiny chance of creating a false positive for passing in the
|
||||
// off-chance the randomly assigned color is the same as the test color.
|
||||
expect($colorPickerPreview.css('background-color')).to.be($userSwatch.css('background-color'));
|
||||
|
||||
// The swatch updates as the test color is picked.
|
||||
fb.setColor(testColorHash);
|
||||
expect($colorPickerPreview.css('background-color')).to.be(testColorRGB);
|
||||
$colorPickerSave.trigger('click');
|
||||
expect($userSwatch.css('background-color')).to.be(testColorRGB);
|
||||
|
||||
// give it a second to save the color 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$;
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
$userButton = chrome$('.buttonicon-showusers');
|
||||
$userButton.trigger('click');
|
||||
|
||||
$userSwatch = chrome$('#myswatch');
|
||||
$userSwatch.trigger('click');
|
||||
|
||||
$colorPickerPreview = chrome$('#mycolorpickerpreview');
|
||||
|
||||
expect($colorPickerPreview.css('background-color')).to.be(testColorRGB);
|
||||
expect($userSwatch.css('background-color')).to.be(testColorRGB);
|
||||
});
|
||||
|
||||
it('Own user color is shown when you enter a chat', function (done) {
|
||||
this.timeout(1000);
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
const $colorOption = helper.padChrome$('#options-colorscheck');
|
||||
if (!$colorOption.is(':checked')) {
|
||||
$colorOption.trigger('click');
|
||||
}
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
const $userButton = chrome$('.buttonicon-showusers');
|
||||
$userButton.trigger('click');
|
||||
|
||||
const $userSwatch = chrome$('#myswatch');
|
||||
$userSwatch.trigger('click');
|
||||
|
||||
const fb = chrome$.farbtastic('#colorpicker');
|
||||
const $colorPickerSave = chrome$('#mycolorpickersave');
|
||||
|
||||
// Same color represented in two different ways
|
||||
const testColorHash = '#abcdef';
|
||||
const testColorRGB = 'rgb(171, 205, 239)';
|
||||
|
||||
fb.setColor(testColorHash);
|
||||
$colorPickerSave.trigger('click');
|
||||
|
||||
// click on the chat button to make chat visible
|
||||
const $chatButton = chrome$('#chaticon');
|
||||
$chatButton.trigger('click');
|
||||
const $chatInput = chrome$('#chatinput');
|
||||
$chatInput.sendkeys('O hi'); // simulate a keypress of typing user
|
||||
$chatInput.sendkeys('{enter}');
|
||||
|
||||
// wait until the chat message shows up
|
||||
helper.waitFor(() => chrome$('#chattext').children('p').length !== 0).done(() => {
|
||||
const $firstChatMessage = chrome$('#chattext').children('p');
|
||||
// expect the first chat message to be of the user's color
|
||||
expect($firstChatMessage.css('background-color')).to.be(testColorRGB);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('font select', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('makes text RobotoMono', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
const $settingsButton = chrome$('.buttonicon-settings');
|
||||
$settingsButton.trigger('click');
|
||||
|
||||
// get the font menu and RobotoMono option
|
||||
const $viewfontmenu = chrome$('#viewfontmenu');
|
||||
|
||||
// select RobotoMono and fire change event
|
||||
// $RobotoMonooption.attr('selected','selected');
|
||||
// commenting out above will break safari test
|
||||
$viewfontmenu.val('RobotoMono');
|
||||
$viewfontmenu.trigger('change');
|
||||
|
||||
// check if font changed to RobotoMono
|
||||
const fontFamily = inner$('body').css('font-family').toLowerCase();
|
||||
const containsStr = fontFamily.indexOf('robotomono');
|
||||
expect(containsStr).to.not.be(-1);
|
||||
});
|
||||
});
|
|
@ -167,7 +167,7 @@ describe('the test helper', function () {
|
|||
expect(Date.now() - before).to.be.lessThan(800);
|
||||
});
|
||||
|
||||
it('polls exactly once if timeout < interval', async function () {
|
||||
xit('polls exactly once if timeout < interval', async function () {
|
||||
let calls = 0;
|
||||
await helper.waitFor(() => { calls++; }, 1, 1000)
|
||||
.fail(() => {}) // Suppress the redundant uncatchable exception.
|
||||
|
@ -249,7 +249,7 @@ describe('the test helper', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('changes editor selection to be between startOffset of $startLine ' +
|
||||
xit('changes editor selection to be between startOffset of $startLine ' +
|
||||
'and endOffset of $endLine', function (done) {
|
||||
const inner$ = helper.padInner$;
|
||||
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('height regression after ace.js refactoring', function () {
|
||||
before(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
// everything fits inside the viewport
|
||||
it('clientHeight should equal scrollHeight with few lines', async function () {
|
||||
await helper.clearPad();
|
||||
const outerHtml = helper.padChrome$('iframe')[0].contentDocument.documentElement;
|
||||
// Give some time for the heights to settle.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(outerHtml.clientHeight).to.be(outerHtml.scrollHeight);
|
||||
});
|
||||
|
||||
it('client height should be less than scrollHeight with many lines', async function () {
|
||||
await helper.clearPad();
|
||||
await helper.edit('Test line\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
|
||||
const outerHtml = helper.padChrome$('iframe')[0].contentDocument.documentElement;
|
||||
// Need to poll because the heights take some time to settle.
|
||||
await helper.waitForPromise(() => outerHtml.clientHeight < outerHtml.scrollHeight);
|
||||
});
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('italic some text', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('makes text italic using button', async function () {
|
||||
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-italic');
|
||||
$boldButton.trigger('click');
|
||||
|
||||
// ace creates a new dom element when you press a button, just get the first text element again
|
||||
const $newFirstTextElement = inner$('div').first();
|
||||
|
||||
// is there a <i> element now?
|
||||
const isItalic = $newFirstTextElement.find('i').length === 1;
|
||||
|
||||
// expect it to be bold
|
||||
expect(isItalic).to.be(true);
|
||||
|
||||
// make sure the text hasn't changed
|
||||
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
|
||||
});
|
||||
|
||||
it('makes text italic using 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.ctrlKey = true; // Control key
|
||||
e.which = 105; // i
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
|
||||
// ace creates a new dom element when you press a button, just get the first text element again
|
||||
const $newFirstTextElement = inner$('div').first();
|
||||
|
||||
// is there a <i> element now?
|
||||
const isItalic = $newFirstTextElement.find('i').length === 1;
|
||||
|
||||
// expect it to be bold
|
||||
expect(isItalic).to.be(true);
|
||||
|
||||
// make sure the text hasn't changed
|
||||
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
|
||||
});
|
||||
});
|
|
@ -1,121 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('Language select and change', function () {
|
||||
// Destroy language cookies
|
||||
window.Cookies.remove('language');
|
||||
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
// Destroy language cookies
|
||||
it('makes text german', async function () {
|
||||
this.timeout(1000);
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
const $settingsButton = chrome$('.buttonicon-settings');
|
||||
$settingsButton.trigger('click');
|
||||
|
||||
// click the language button
|
||||
const $language = chrome$('#languagemenu');
|
||||
const $languageoption = $language.find('[value=de]');
|
||||
|
||||
// select german
|
||||
$languageoption.attr('selected', 'selected');
|
||||
$language.trigger('change');
|
||||
|
||||
await helper.waitForPromise(
|
||||
() => chrome$('.buttonicon-bold').parent()[0].title === 'Fett (Strg-B)');
|
||||
|
||||
// get the value of the bold button
|
||||
const $boldButton = chrome$('.buttonicon-bold').parent();
|
||||
|
||||
// get the title of the bold button
|
||||
const boldButtonTitle = $boldButton[0].title;
|
||||
|
||||
// check if the language is now german
|
||||
expect(boldButtonTitle).to.be('Fett (Strg-B)');
|
||||
});
|
||||
|
||||
it('makes text English', async function () {
|
||||
this.timeout(1000);
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
const $settingsButton = chrome$('.buttonicon-settings');
|
||||
$settingsButton.trigger('click');
|
||||
|
||||
// click the language button
|
||||
const $language = chrome$('#languagemenu');
|
||||
// select english
|
||||
$language.val('en');
|
||||
$language.trigger('change');
|
||||
|
||||
// get the value of the bold button
|
||||
let $boldButton = chrome$('.buttonicon-bold').parent();
|
||||
|
||||
await helper.waitForPromise(() => $boldButton[0].title !== 'Fett (Strg+B)');
|
||||
|
||||
// get the value of the bold button
|
||||
$boldButton = chrome$('.buttonicon-bold').parent();
|
||||
|
||||
// get the title of the bold button
|
||||
const boldButtonTitle = $boldButton[0].title;
|
||||
|
||||
// check if the language is now English
|
||||
expect(boldButtonTitle).to.be('Bold (Ctrl+B)');
|
||||
});
|
||||
|
||||
it('changes direction when picking an rtl lang', async function () {
|
||||
// TODO: flaky
|
||||
if (window.bowser.safari) {
|
||||
this.timeout(5000);
|
||||
} else {
|
||||
this.timeout(1000);
|
||||
}
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
const $settingsButton = chrome$('.buttonicon-settings');
|
||||
$settingsButton.trigger('click');
|
||||
|
||||
// click the language button
|
||||
const $language = chrome$('#languagemenu');
|
||||
const $languageoption = $language.find('[value=ar]');
|
||||
|
||||
// select arabic
|
||||
// $languageoption.attr('selected','selected'); // Breaks the test..
|
||||
$language.val('ar');
|
||||
$languageoption.trigger('change');
|
||||
|
||||
await helper.waitForPromise(() => chrome$('html')[0].dir !== 'ltr');
|
||||
|
||||
// check if the document's direction was changed
|
||||
expect(chrome$('html')[0].dir).to.be('rtl');
|
||||
});
|
||||
|
||||
it('changes direction when picking an ltr lang', async function () {
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// click on the settings button to make settings visible
|
||||
const $settingsButton = chrome$('.buttonicon-settings');
|
||||
$settingsButton.trigger('click');
|
||||
|
||||
// click the language button
|
||||
const $language = chrome$('#languagemenu');
|
||||
const $languageoption = $language.find('[value=en]');
|
||||
|
||||
// select english
|
||||
// select arabic
|
||||
$languageoption.attr('selected', 'selected');
|
||||
$language.val('en');
|
||||
$languageoption.trigger('change');
|
||||
|
||||
await helper.waitForPromise(() => chrome$('html')[0].dir !== 'rtl');
|
||||
|
||||
// check if the document's direction was changed
|
||||
expect(chrome$('html')[0].dir).to.be('ltr');
|
||||
});
|
||||
});
|
|
@ -1,233 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('ordered_list.js', function () {
|
||||
describe('assign ordered list', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('inserts ordered list text', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||
$insertorderedlistButton.trigger('click');
|
||||
|
||||
await helper.waitForPromise(() => inner$('div').first().find('ol li').length === 1);
|
||||
});
|
||||
|
||||
context('when user presses Ctrl+Shift+N', function () {
|
||||
context('and pad shortcut is enabled', function () {
|
||||
beforeEach(async function () {
|
||||
const originalHTML = helper.padInner$('body').html();
|
||||
makeSureShortcutIsEnabled('cmdShiftN');
|
||||
triggerCtrlShiftShortcut('N');
|
||||
await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML);
|
||||
});
|
||||
|
||||
it('inserts unordered list', async function () {
|
||||
await helper.waitForPromise(
|
||||
() => helper.padInner$('div').first().find('ol li').length === 1);
|
||||
});
|
||||
});
|
||||
|
||||
context('and pad shortcut is disabled', function () {
|
||||
beforeEach(async function () {
|
||||
const originalHTML = helper.padInner$('body').html();
|
||||
makeSureShortcutIsDisabled('cmdShiftN');
|
||||
triggerCtrlShiftShortcut('N');
|
||||
try {
|
||||
// The HTML should not change. Briefly wait for it to change and fail if it does change.
|
||||
await helper.waitForPromise(
|
||||
() => helper.padInner$('body').html() !== originalHTML, 500);
|
||||
} catch (err) {
|
||||
// We want the test to pass if the above wait timed out. (If it timed out that
|
||||
// means the HTML never changed, which is a good thing.)
|
||||
// TODO: Re-throw non-"condition never became true" errors to avoid false positives.
|
||||
}
|
||||
// This will fail if the above `waitForPromise()` succeeded.
|
||||
expect(helper.padInner$('body').html()).to.be(originalHTML);
|
||||
});
|
||||
|
||||
it('does not insert unordered list', async function () {
|
||||
this.timeout(3000);
|
||||
try {
|
||||
await helper.waitForPromise(
|
||||
() => helper.padInner$('div').first().find('ol li').length === 1);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
expect().fail('Unordered list inserted, should ignore shortcut');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('when user presses Ctrl+Shift+1', function () {
|
||||
context('and pad shortcut is enabled', function () {
|
||||
beforeEach(async function () {
|
||||
const originalHTML = helper.padInner$('body').html();
|
||||
makeSureShortcutIsEnabled('cmdShift1');
|
||||
triggerCtrlShiftShortcut('1');
|
||||
await helper.waitForPromise(() => helper.padInner$('body').html() !== originalHTML);
|
||||
});
|
||||
|
||||
it('inserts unordered list', async function () {
|
||||
helper.waitForPromise(() => helper.padInner$('div').first().find('ol li').length === 1);
|
||||
});
|
||||
});
|
||||
|
||||
context('and pad shortcut is disabled', function () {
|
||||
beforeEach(async function () {
|
||||
const originalHTML = helper.padInner$('body').html();
|
||||
makeSureShortcutIsDisabled('cmdShift1');
|
||||
triggerCtrlShiftShortcut('1');
|
||||
try {
|
||||
// The HTML should not change. Briefly wait for it to change and fail if it does change.
|
||||
await helper.waitForPromise(
|
||||
() => helper.padInner$('body').html() !== originalHTML, 500);
|
||||
} catch (err) {
|
||||
// We want the test to pass if the above wait timed out. (If it timed out that
|
||||
// means the HTML never changed, which is a good thing.)
|
||||
// TODO: Re-throw non-"condition never became true" errors to avoid false positives.
|
||||
}
|
||||
// This will fail if the above `waitForPromise()` succeeded.
|
||||
expect(helper.padInner$('body').html()).to.be(originalHTML);
|
||||
});
|
||||
|
||||
it('does not insert unordered list', async function () {
|
||||
this.timeout(3000);
|
||||
try {
|
||||
await helper.waitForPromise(
|
||||
() => helper.padInner$('div').first().find('ol li').length === 1);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
expect().fail('Unordered list inserted, should ignore shortcut');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('issue #4748 keeps numbers increment on OL', async function () {
|
||||
this.timeout(5000);
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||
const $firstLine = inner$('div').first();
|
||||
$firstLine.sendkeys('{selectall}');
|
||||
$insertorderedlistButton.trigger('click');
|
||||
const $secondLine = inner$('div').first().next();
|
||||
$secondLine.sendkeys('{selectall}');
|
||||
$insertorderedlistButton.trigger('click');
|
||||
expect($secondLine.find('ol').attr('start') === 2);
|
||||
});
|
||||
|
||||
xit('issue #1125 keeps the numbered list on enter for the new line', async function () {
|
||||
// EMULATES PASTING INTO A PAD
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||
$insertorderedlistButton.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.waitForPromise(() => inner$('div span').first().text().indexOf('line 2') === -1);
|
||||
|
||||
const $newSecondLine = inner$('div').first().next();
|
||||
const hasOLElement = $newSecondLine.find('ol li').length === 1;
|
||||
expect(hasOLElement).to.be(true);
|
||||
expect($newSecondLine.text()).to.be('line 2');
|
||||
const hasLineNumber = $newSecondLine.find('ol').attr('start') === 2;
|
||||
// This doesn't work because pasting in content doesn't work
|
||||
expect(hasLineNumber).to.be(true);
|
||||
});
|
||||
|
||||
const triggerCtrlShiftShortcut = (shortcutChar) => {
|
||||
const inner$ = helper.padInner$;
|
||||
const e = new inner$.Event(helper.evtType);
|
||||
e.ctrlKey = true;
|
||||
e.shiftKey = true;
|
||||
e.which = shortcutChar.toString().charCodeAt(0);
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
};
|
||||
|
||||
const makeSureShortcutIsDisabled = (shortcut) => {
|
||||
helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false;
|
||||
};
|
||||
const makeSureShortcutIsEnabled = (shortcut) => {
|
||||
helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = true;
|
||||
};
|
||||
});
|
||||
|
||||
describe('Pressing Tab in an OL increases and decreases indentation', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('indent and de-indent list item with keypress', async function () {
|
||||
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}');
|
||||
|
||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||
$insertorderedlistButton.trigger('click');
|
||||
|
||||
const e = new inner$.Event(helper.evtType);
|
||||
e.keyCode = 9; // tab
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
|
||||
expect(inner$('div').first().find('.list-number2').length === 1).to.be(true);
|
||||
e.shiftKey = true; // shift
|
||||
e.keyCode = 9; // tab
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
|
||||
await helper.waitForPromise(() => inner$('div').first().find('.list-number1').length === 1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Pressing indent/outdent button in an OL increases and ' +
|
||||
'decreases indentation and bullet / ol formatting', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('indent and de-indent list item with indent button', async function () {
|
||||
this.timeout(1000);
|
||||
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}');
|
||||
|
||||
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
|
||||
$insertorderedlistButton.trigger('click');
|
||||
|
||||
const $indentButton = chrome$('.buttonicon-indent');
|
||||
$indentButton.trigger('click'); // make it indented twice
|
||||
|
||||
expect(inner$('div').first().find('.list-number2').length === 1).to.be(true);
|
||||
|
||||
const $outdentButton = chrome$('.buttonicon-outdent');
|
||||
$outdentButton.trigger('click'); // make it deindented to 1
|
||||
|
||||
await helper.waitForPromise(() => inner$('div').first().find('.list-number1').length === 1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,59 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('undo button then redo button', function () {
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('redo some typing with button', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const $firstTextElement = inner$('div span').first();
|
||||
const originalValue = $firstTextElement.text(); // get the original value
|
||||
const newString = 'Foo';
|
||||
|
||||
$firstTextElement.sendkeys(newString); // send line 1 to the pad
|
||||
const modifiedValue = $firstTextElement.text(); // get the modified value
|
||||
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
|
||||
|
||||
// get undo and redo buttons
|
||||
const $undoButton = chrome$('.buttonicon-undo');
|
||||
const $redoButton = chrome$('.buttonicon-redo');
|
||||
// click the buttons
|
||||
$undoButton.trigger('click'); // removes foo
|
||||
$redoButton.trigger('click'); // resends foo
|
||||
|
||||
await helper.waitForPromise(() => inner$('div span').first().text() === newString);
|
||||
const finalValue = inner$('div').first().text();
|
||||
expect(finalValue).to.be(modifiedValue); // expect the value to change
|
||||
});
|
||||
|
||||
it('redo some typing with keypress', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const $firstTextElement = inner$('div span').first();
|
||||
const originalValue = $firstTextElement.text(); // get the original value
|
||||
const newString = 'Foo';
|
||||
|
||||
$firstTextElement.sendkeys(newString); // send line 1 to the pad
|
||||
const modifiedValue = $firstTextElement.text(); // get the modified value
|
||||
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
|
||||
|
||||
let e = new inner$.Event(helper.evtType);
|
||||
e.ctrlKey = true; // Control key
|
||||
e.which = 90; // z
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
|
||||
e = new inner$.Event(helper.evtType);
|
||||
e.ctrlKey = true; // Control key
|
||||
e.which = 121; // y
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
|
||||
await helper.waitForPromise(() => inner$('div span').first().text() === newString);
|
||||
const finalValue = inner$('div').first().text();
|
||||
expect(finalValue).to.be(modifiedValue); // expect the value to change
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('strikethrough button', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('makes text strikethrough', async function () {
|
||||
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 strikethrough button and click it
|
||||
const $strikethroughButton = chrome$('.buttonicon-strikethrough');
|
||||
$strikethroughButton.trigger('click');
|
||||
|
||||
// ace creates a new dom element when you press a button, just get the first text element again
|
||||
const $newFirstTextElement = inner$('div').first();
|
||||
|
||||
// is there a <i> element now?
|
||||
const isstrikethrough = $newFirstTextElement.find('s').length === 1;
|
||||
|
||||
// expect it to be strikethrough
|
||||
expect(isstrikethrough).to.be(true);
|
||||
|
||||
// make sure the text hasn't changed
|
||||
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
|
||||
});
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// deactivated, we need a nice way to get the timeslider, this is ugly
|
||||
xdescribe('timeslider button takes you to the timeslider of a pad', function () {
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('timeslider contained in URL', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const $firstTextElement = inner$('div span').first();
|
||||
const originalValue = $firstTextElement.text(); // get the original value
|
||||
$firstTextElement.sendkeys('Testing'); // send line 1 to the pad
|
||||
|
||||
const modifiedValue = $firstTextElement.text(); // get the modified value
|
||||
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
|
||||
|
||||
// The value has changed so we can..
|
||||
await helper.waitForPromise(() => modifiedValue !== originalValue);
|
||||
|
||||
const $timesliderButton = chrome$('#timesliderlink');
|
||||
$timesliderButton.trigger('click'); // So click the timeslider link
|
||||
|
||||
await helper.waitForPromise(() => {
|
||||
const iFrameURL = chrome$.window.location.href;
|
||||
if (iFrameURL) {
|
||||
return iFrameURL.indexOf('timeslider') !== -1;
|
||||
} else {
|
||||
return false; // the URL hasnt been set yet
|
||||
}
|
||||
});
|
||||
|
||||
// click the buttons
|
||||
const iFrameURL = chrome$.window.location.href; // get the url
|
||||
const inTimeslider = iFrameURL.indexOf('timeslider') !== -1;
|
||||
expect(inTimeslider).to.be(true); // expect the value to change
|
||||
});
|
||||
});
|
|
@ -1,101 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('timeslider follow', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
// TODO needs test if content is also followed, when user a makes edits
|
||||
// while user b is in the timeslider
|
||||
it("content as it's added to timeslider", async function () {
|
||||
this.timeout(20000);
|
||||
// send 6 revisions
|
||||
const revs = 6;
|
||||
const message = 'a\n\n\n\n\n\n\n\n\n\n';
|
||||
const newLines = message.split('\n').length;
|
||||
for (let i = 0; i < revs; i++) {
|
||||
await helper.edit(message, newLines * i + 1);
|
||||
}
|
||||
|
||||
await helper.gotoTimeslider(0);
|
||||
await helper.waitForPromise(() => helper.contentWindow().location.hash === '#0');
|
||||
|
||||
const originalTop = helper.contentWindow().$('#innerdocbody').offset();
|
||||
|
||||
// set to follow contents as it arrives
|
||||
helper.contentWindow().$('#options-followContents').prop('checked', true);
|
||||
helper.contentWindow().$('#playpause_button_icon').trigger('click');
|
||||
|
||||
let newTop;
|
||||
await helper.waitForPromise(() => {
|
||||
newTop = helper.contentWindow().$('#innerdocbody').offset();
|
||||
return newTop.top < originalTop.top;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests for bug described in #4389
|
||||
* The goal is to scroll to the first line that contains a change right before
|
||||
* the change is applied.
|
||||
*/
|
||||
it('only to lines that exist in the pad view, regression test for #4389', async function () {
|
||||
await helper.clearPad();
|
||||
await helper.edit('Test line\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
|
||||
await helper.edit('Another test line', 40);
|
||||
|
||||
|
||||
await helper.gotoTimeslider();
|
||||
|
||||
// set to follow contents as it arrives
|
||||
helper.contentWindow().$('#options-followContents').prop('checked', true);
|
||||
|
||||
const oldYPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop;
|
||||
expect(oldYPosition).to.be(0);
|
||||
|
||||
/**
|
||||
* pad content rev 0 [default Pad text]
|
||||
* pad content rev 1 ['']
|
||||
* pad content rev 2 ['Test line','','', ..., '']
|
||||
* pad content rev 3 ['Test line','',..., 'Another test line', ..., '']
|
||||
*/
|
||||
|
||||
// line 40 changed
|
||||
helper.contentWindow().$('#leftstep').trigger('click');
|
||||
await helper.waitForPromise(() => hasFollowedToLine(40));
|
||||
|
||||
// line 1 is the first line that changed
|
||||
helper.contentWindow().$('#leftstep').trigger('click');
|
||||
await helper.waitForPromise(() => hasFollowedToLine(1));
|
||||
|
||||
// line 1 changed
|
||||
helper.contentWindow().$('#leftstep').trigger('click');
|
||||
await helper.waitForPromise(() => hasFollowedToLine(1));
|
||||
|
||||
// line 1 changed
|
||||
helper.contentWindow().$('#rightstep').trigger('click');
|
||||
await helper.waitForPromise(() => hasFollowedToLine(1));
|
||||
|
||||
// line 1 is the first line that changed
|
||||
helper.contentWindow().$('#rightstep').trigger('click');
|
||||
await helper.waitForPromise(() => hasFollowedToLine(1));
|
||||
|
||||
// line 40 changed
|
||||
helper.contentWindow().$('#rightstep').trigger('click');
|
||||
helper.waitForPromise(() => hasFollowedToLine(40));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {number} lineNum
|
||||
* @returns {boolean} scrolled to the lineOffset?
|
||||
*/
|
||||
const hasFollowedToLine = (lineNum) => {
|
||||
const scrollPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop;
|
||||
const lineOffset =
|
||||
helper.contentWindow().$('#innerdocbody').find(`div:nth-child(${lineNum})`)[0].offsetTop;
|
||||
return Math.abs(scrollPosition - lineOffset) < 1;
|
||||
};
|
|
@ -1,46 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('undo button', function () {
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('undo some typing by clicking undo button', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const $firstTextElement = inner$('div span').first();
|
||||
const originalValue = $firstTextElement.text(); // get the original value
|
||||
|
||||
$firstTextElement.sendkeys('foo'); // send line 1 to the pad
|
||||
const modifiedValue = $firstTextElement.text(); // get the modified value
|
||||
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
|
||||
|
||||
// get clear authorship button as a variable
|
||||
const $undoButton = chrome$('.buttonicon-undo');
|
||||
// click the button
|
||||
$undoButton.trigger('click');
|
||||
|
||||
await helper.waitForPromise(() => inner$('div span').first().text() === originalValue);
|
||||
});
|
||||
|
||||
it('undo some typing using a keypress', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
|
||||
// get the first text element inside the editable space
|
||||
const $firstTextElement = inner$('div span').first();
|
||||
const originalValue = $firstTextElement.text(); // get the original value
|
||||
|
||||
$firstTextElement.sendkeys('foo'); // send line 1 to the pad
|
||||
const modifiedValue = $firstTextElement.text(); // get the modified value
|
||||
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
|
||||
|
||||
const e = new inner$.Event(helper.evtType);
|
||||
e.ctrlKey = true; // Control key
|
||||
e.which = 90; // z
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
|
||||
await helper.waitForPromise(() => inner$('div span').first().text() === originalValue);
|
||||
});
|
||||
});
|
|
@ -1,146 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('unordered_list.js', function () {
|
||||
describe('assign unordered list', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('insert unordered list text then removes by outdent', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
const originalText = inner$('div').first().text();
|
||||
|
||||
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||
$insertunorderedlistButton.trigger('click');
|
||||
|
||||
await helper.waitForPromise(() => {
|
||||
const newText = inner$('div').first().text();
|
||||
return newText === originalText && inner$('div').first().find('ul li').length === 1;
|
||||
});
|
||||
|
||||
// remove indentation by bullet and ensure text string remains the same
|
||||
chrome$('.buttonicon-outdent').trigger('click');
|
||||
await helper.waitForPromise(() => inner$('div').first().text() === originalText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unassign unordered list', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('insert unordered list text then remove by clicking list again', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
const originalText = inner$('div').first().text();
|
||||
|
||||
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||
$insertunorderedlistButton.trigger('click');
|
||||
|
||||
await helper.waitForPromise(() => {
|
||||
const newText = inner$('div').first().text();
|
||||
return newText === originalText && inner$('div').first().find('ul li').length === 1;
|
||||
});
|
||||
|
||||
// remove indentation by bullet and ensure text string remains the same
|
||||
$insertunorderedlistButton.trigger('click');
|
||||
await helper.waitForPromise(() => inner$('div').find('ul').length !== 1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('keep unordered list on enter key', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('Keeps the unordered list on enter for the new line', async function () {
|
||||
const inner$ = helper.padInner$;
|
||||
const chrome$ = helper.padChrome$;
|
||||
|
||||
const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||
$insertorderedlistButton.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.waitForPromise(() => 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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pressing Tab in an UL increases and decreases indentation', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('indent and de-indent list item with keypress', async function () {
|
||||
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}');
|
||||
|
||||
const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||
$insertorderedlistButton.trigger('click');
|
||||
|
||||
const e = new inner$.Event(helper.evtType);
|
||||
e.keyCode = 9; // tab
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
|
||||
expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true);
|
||||
e.shiftKey = true; // shift
|
||||
e.keyCode = 9; // tab
|
||||
inner$('#innerdocbody').trigger(e);
|
||||
|
||||
await helper.waitForPromise(() => inner$('div').first().find('.list-bullet1').length === 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pressing indent/outdent button in an UL increases and decreases indentation ' +
|
||||
'and bullet / ol formatting', function () {
|
||||
// create a new pad before each test run
|
||||
beforeEach(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
it('indent and de-indent list item with indent button', async function () {
|
||||
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}');
|
||||
|
||||
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
|
||||
$insertunorderedlistButton.trigger('click');
|
||||
|
||||
const $indentButton = chrome$('.buttonicon-indent');
|
||||
$indentButton.trigger('click'); // make it indented twice
|
||||
|
||||
expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true);
|
||||
const $outdentButton = chrome$('.buttonicon-outdent');
|
||||
$outdentButton.trigger('click'); // make it deindented to 1
|
||||
|
||||
await helper.waitForPromise(() => inner$('div').first().find('.list-bullet1').length === 1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('urls', function () {
|
||||
// Returns the first text element. Note that any change to the text element will result in the
|
||||
// element being replaced with another object.
|
||||
const txt = () => helper.padInner$('div').first();
|
||||
|
||||
before(async function () {
|
||||
await helper.aNewPad();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
await helper.clearPad();
|
||||
});
|
||||
|
||||
describe('entering a URL makes a link', function () {
|
||||
for (const url of ['https://etherpad.org', 'www.etherpad.org']) {
|
||||
it(url, async function () {
|
||||
this.timeout(5000);
|
||||
const url = 'https://etherpad.org';
|
||||
await helper.edit(url);
|
||||
await helper.waitForPromise(() => txt().find('a').length === 1, 2000);
|
||||
const link = txt().find('a');
|
||||
expect(link.attr('href')).to.be(url);
|
||||
expect(link.text()).to.be(url);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('special characters inside URL', function () {
|
||||
for (const char of '-:@_.,~%+/?=&#!;()[]$\'*') {
|
||||
const url = `https://etherpad.org/${char}foo`;
|
||||
it(url, async function () {
|
||||
await helper.edit(url);
|
||||
await helper.waitForPromise(() => txt().find('a').length === 1);
|
||||
const link = txt().find('a');
|
||||
expect(link.attr('href')).to.be(url);
|
||||
expect(link.text()).to.be(url);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('punctuation after URL is ignored', function () {
|
||||
for (const char of ':.,;?!)]\'*') {
|
||||
const want = 'https://etherpad.org';
|
||||
const input = want + char;
|
||||
it(input, async function () {
|
||||
await helper.edit(input);
|
||||
await helper.waitForPromise(() => txt().find('a').length === 1);
|
||||
const link = txt().find('a');
|
||||
expect(link.attr('href')).to.be(want);
|
||||
expect(link.text()).to.be(want);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
654
src/tests/settings.json
Normal file
654
src/tests/settings.json
Normal file
|
@ -0,0 +1,654 @@
|
|||
/*
|
||||
* This file must be valid JSON. But comments are allowed
|
||||
*
|
||||
* Please edit settings.json, not settings.json.template
|
||||
*
|
||||
* Please note that starting from Etherpad 1.6.0 you can store DB credentials in
|
||||
* a separate file (credentials.json).
|
||||
*
|
||||
*
|
||||
* ENVIRONMENT VARIABLE SUBSTITUTION
|
||||
* =================================
|
||||
*
|
||||
* All the configuration values can be read from environment variables using the
|
||||
* syntax "${ENV_VAR}" or "${ENV_VAR:default_value}".
|
||||
*
|
||||
* This is useful, for example, when running in a Docker container.
|
||||
*
|
||||
* DETAILED RULES:
|
||||
* - If the environment variable is set to the string "true" or "false", the
|
||||
* value becomes Boolean true or false.
|
||||
* - If the environment variable is set to the string "null", the value
|
||||
* becomes null.
|
||||
* - If the environment variable is set to the string "undefined", the setting
|
||||
* is removed entirely, except when used as the member of an array in which
|
||||
* case it becomes null.
|
||||
* - If the environment variable is set to a string representation of a finite
|
||||
* number, the string is converted to that number.
|
||||
* - If the environment variable is set to any other string, including the
|
||||
* empty string, the value is that string.
|
||||
* - If the environment variable is unset and a default value is provided, the
|
||||
* value is as if the environment variable was set to the provided default:
|
||||
* - "${UNSET_VAR:}" becomes the empty string.
|
||||
* - "${UNSET_VAR:foo}" becomes the string "foo".
|
||||
* - "${UNSET_VAR:true}" and "${UNSET_VAR:false}" become true and false.
|
||||
* - "${UNSET_VAR:null}" becomes null.
|
||||
* - "${UNSET_VAR:undefined}" causes the setting to be removed (or be set
|
||||
* to null, if used as a member of an array).
|
||||
* - If the environment variable is unset and no default value is provided,
|
||||
* the value becomes null. THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF
|
||||
* ETHERPAD; if you want the default value to be null, you should explicitly
|
||||
* specify "null" as the default value.
|
||||
*
|
||||
* EXAMPLE:
|
||||
* "port": "${PORT:9001}"
|
||||
* "minify": "${MINIFY}"
|
||||
* "skinName": "${SKIN_NAME:colibris}"
|
||||
*
|
||||
* Would read the configuration values for those items from the environment
|
||||
* variables PORT, MINIFY and SKIN_NAME.
|
||||
*
|
||||
* If PORT and SKIN_NAME variables were not defined, the default values 9001 and
|
||||
* "colibris" would be used.
|
||||
* The configuration value "minify", on the other hand, does not have a
|
||||
* designated default value. Thus, if the environment variable MINIFY were
|
||||
* undefined, "minify" would be null.
|
||||
*
|
||||
* REMARKS:
|
||||
* 1) please note that variable substitution always needs to be quoted.
|
||||
*
|
||||
* "port": 9001, <-- Literal values. When not using
|
||||
* "minify": false substitution, only strings must be
|
||||
* "skinName": "colibris" quoted. Booleans and numbers must not.
|
||||
*
|
||||
* "port": "${PORT:9001}" <-- CORRECT: if you want to use a variable
|
||||
* "minify": "${MINIFY:true}" substitution, put quotes around its name,
|
||||
* "skinName": "${SKIN_NAME}" even if the required value is a number or
|
||||
* a boolean.
|
||||
* Etherpad will take care of rewriting it
|
||||
* to the proper type if necessary.
|
||||
*
|
||||
* "port": ${PORT:9001} <-- ERROR: this is not valid json. Quotes
|
||||
* "minify": ${MINIFY} around variable names are missing.
|
||||
* "skinName": ${SKIN_NAME}
|
||||
*
|
||||
* 2) Beware of undefined variables and default values: nulls and empty strings
|
||||
* are different!
|
||||
*
|
||||
* This is particularly important for user's passwords (see the relevant
|
||||
* section):
|
||||
*
|
||||
* "password": "${PASSW}" // if PASSW is not defined would result in password === null
|
||||
* "password": "${PASSW:}" // if PASSW is not defined would result in password === ''
|
||||
*
|
||||
* If you want to use an empty value (null) as default value for a variable,
|
||||
* simply do not set it, without putting any colons: "${ABIWORD}".
|
||||
*
|
||||
* 3) if you want to use newlines in the default value of a string parameter,
|
||||
* use "\n" as usual.
|
||||
*
|
||||
* "defaultPadText" : "${DEFAULT_PAD_TEXT}Line 1\nLine 2"
|
||||
*/
|
||||
{
|
||||
/*
|
||||
* Name your instance!
|
||||
*/
|
||||
"title": "Etherpad",
|
||||
|
||||
/*
|
||||
* Pathname of the favicon you want to use. If null, the skin's favicon is
|
||||
* used if one is provided by the skin, otherwise the default Etherpad favicon
|
||||
* is used. If this is a relative path it is interpreted as relative to the
|
||||
* Etherpad root directory.
|
||||
*/
|
||||
"favicon": null,
|
||||
|
||||
/*
|
||||
* Skin name.
|
||||
*
|
||||
* Its value has to be an existing directory under src/static/skins.
|
||||
* You can write your own, or use one of the included ones:
|
||||
*
|
||||
* - "no-skin": an empty skin (default). This yields the unmodified,
|
||||
* traditional Etherpad theme.
|
||||
* - "colibris": the new experimental skin (since Etherpad 1.8), candidate to
|
||||
* become the default in Etherpad 2.0
|
||||
*/
|
||||
"skinName": "colibris",
|
||||
|
||||
/*
|
||||
* Skin Variants
|
||||
*
|
||||
* Use the UI skin variants builder at /p/test#skinvariantsbuilder
|
||||
*
|
||||
* For the colibris skin only, you can choose how to render the three main
|
||||
* containers:
|
||||
* - toolbar (top menu with icons)
|
||||
* - editor (containing the text of the pad)
|
||||
* - background (area outside of editor, mostly visible when using page style)
|
||||
*
|
||||
* For each of the 3 containers you can choose 4 color combinations:
|
||||
* super-light, light, dark, super-dark.
|
||||
*
|
||||
* For example, to make the toolbar dark, you will include "dark-toolbar" into
|
||||
* skinVariants.
|
||||
*
|
||||
* You can provide multiple skin variants separated by spaces. Default
|
||||
* skinVariant is "super-light-toolbar super-light-editor light-background".
|
||||
*
|
||||
* For the editor container, you can also make it full width by adding
|
||||
* "full-width-editor" variant (by default editor is rendered as a page, with
|
||||
* a max-width of 900px).
|
||||
*/
|
||||
"skinVariants": "super-light-toolbar super-light-editor light-background",
|
||||
|
||||
/*
|
||||
* IP and port which Etherpad should bind at.
|
||||
*
|
||||
* Binding to a Unix socket is also supported: just use an empty string for
|
||||
* the ip, and put the full path to the socket in the port parameter.
|
||||
*
|
||||
* EXAMPLE USING UNIX SOCKET:
|
||||
* "ip": "", // <-- has to be an empty string
|
||||
* "port" : "/somepath/etherpad.socket", // <-- path to a Unix socket
|
||||
*/
|
||||
"ip": "0.0.0.0",
|
||||
"port": 9001,
|
||||
|
||||
/*
|
||||
* Option to hide/show the settings.json in admin page.
|
||||
*
|
||||
* Default option is set to true
|
||||
*/
|
||||
"showSettingsInAdminPage": true,
|
||||
|
||||
/*
|
||||
* Node native SSL support
|
||||
*
|
||||
* This is disabled by default.
|
||||
* Make sure to have the minimum and correct file access permissions set so
|
||||
* that the Etherpad server can access them
|
||||
*/
|
||||
|
||||
/*
|
||||
"ssl" : {
|
||||
"key" : "/path-to-your/epl-server.key",
|
||||
"cert" : "/path-to-your/epl-server.crt",
|
||||
"ca": ["/path-to-your/epl-intermediate-cert1.crt", "/path-to-your/epl-intermediate-cert2.crt"]
|
||||
},
|
||||
*/
|
||||
|
||||
/*
|
||||
* The type of the database.
|
||||
*
|
||||
* You can choose between many DB drivers, for example: dirty, postgres,
|
||||
* sqlite, mysql.
|
||||
*
|
||||
* You shouldn't use "dirty" for for anything else than testing or
|
||||
* development.
|
||||
*
|
||||
*
|
||||
* Database specific settings are dependent on dbType, and go in dbSettings.
|
||||
* Remember that since Etherpad 1.6.0 you can also store this information in
|
||||
* credentials.json.
|
||||
*
|
||||
* For a complete list of the supported drivers, please refer to:
|
||||
* https://www.npmjs.com/package/ueberdb2
|
||||
*/
|
||||
|
||||
"dbType": "dirty",
|
||||
"dbSettings": {
|
||||
"filename": "var/dirty.db"
|
||||
},
|
||||
|
||||
/*
|
||||
* An Example of MySQL Configuration (commented out).
|
||||
*
|
||||
* See: https://github.com/ether/etherpad-lite/wiki/How-to-use-Etherpad-Lite-with-MySQL
|
||||
*/
|
||||
|
||||
/*
|
||||
"dbType" : "mysql",
|
||||
"dbSettings" : {
|
||||
"user": "etherpaduser",
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"password": "PASSWORD",
|
||||
"database": "etherpad_lite_db",
|
||||
"charset": "utf8mb4"
|
||||
},
|
||||
*/
|
||||
|
||||
/*
|
||||
* The default text of a pad
|
||||
*/
|
||||
"defaultPadText" : "Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https:\/\/etherpad.org\n",
|
||||
|
||||
/*
|
||||
* Default Pad behavior.
|
||||
*
|
||||
* Change them if you want to override.
|
||||
*/
|
||||
"padOptions": {
|
||||
"noColors": false,
|
||||
"showControls": true,
|
||||
"showChat": true,
|
||||
"showLineNumbers": true,
|
||||
"useMonospaceFont": false,
|
||||
"userName": null,
|
||||
"userColor": null,
|
||||
"rtl": false,
|
||||
"alwaysShowChat": false,
|
||||
"chatAndUsers": false,
|
||||
"lang": null
|
||||
},
|
||||
|
||||
/*
|
||||
* Pad Shortcut Keys
|
||||
*/
|
||||
"padShortcutEnabled" : {
|
||||
"altF9": true, /* focus on the File Menu and/or editbar */
|
||||
"altC": true, /* focus on the Chat window */
|
||||
"cmdShift2": true, /* shows a gritter popup showing a line author */
|
||||
"delete": true,
|
||||
"return": true,
|
||||
"esc": true, /* in mozilla versions 14-19 avoid reconnecting pad */
|
||||
"cmdS": true, /* save a revision */
|
||||
"tab": true, /* indent */
|
||||
"cmdZ": true, /* undo/redo */
|
||||
"cmdY": true, /* redo */
|
||||
"cmdI": true, /* italic */
|
||||
"cmdB": true, /* bold */
|
||||
"cmdU": true, /* underline */
|
||||
"cmd5": true, /* strike through */
|
||||
"cmdShiftL": true, /* unordered list */
|
||||
"cmdShiftN": true, /* ordered list */
|
||||
"cmdShift1": true, /* ordered list */
|
||||
"cmdShiftC": true, /* clear authorship */
|
||||
"cmdH": true, /* backspace */
|
||||
"ctrlHome": true, /* scroll to top of pad */
|
||||
"pageUp": true,
|
||||
"pageDown": true
|
||||
},
|
||||
|
||||
/*
|
||||
* Should we suppress errors from being visible in the default Pad Text?
|
||||
*/
|
||||
"suppressErrorsInPadText": false,
|
||||
|
||||
/*
|
||||
* If this option is enabled, a user must have a session to access pads.
|
||||
* This effectively allows only group pads to be accessed.
|
||||
*/
|
||||
"requireSession": false,
|
||||
|
||||
/*
|
||||
* Users may edit pads but not create new ones.
|
||||
*
|
||||
* Pad creation is only via the API.
|
||||
* This applies both to group pads and regular pads.
|
||||
*/
|
||||
"editOnly": false,
|
||||
|
||||
/*
|
||||
* If true, all css & js will be minified before sending to the client.
|
||||
*
|
||||
* This will improve the loading performance massively, but makes it difficult
|
||||
* to debug the javascript/css
|
||||
*/
|
||||
"minify": true,
|
||||
|
||||
/*
|
||||
* How long may clients use served javascript code (in seconds)?
|
||||
*
|
||||
* Not setting this may cause problems during deployment.
|
||||
* Set to 0 to disable caching.
|
||||
*/
|
||||
"maxAge": 21600, // 60 * 60 * 6 = 6 hours
|
||||
|
||||
/*
|
||||
* Absolute path to the Abiword executable.
|
||||
*
|
||||
* Abiword is needed to get advanced import/export features of pads. Setting
|
||||
* it to null disables Abiword and will only allow plain text and HTML
|
||||
* import/exports.
|
||||
*/
|
||||
"abiword": null,
|
||||
|
||||
/*
|
||||
* This is the absolute path to the soffice executable.
|
||||
*
|
||||
* LibreOffice can be used in lieu of Abiword to export pads.
|
||||
* Setting it to null disables LibreOffice exporting.
|
||||
*/
|
||||
"soffice": null,
|
||||
|
||||
/*
|
||||
* Allow import of file types other than the supported ones:
|
||||
* txt, doc, docx, rtf, odt, html & htm
|
||||
*/
|
||||
"allowUnknownFileEnds": true,
|
||||
|
||||
/*
|
||||
* This setting is used if you require authentication of all users.
|
||||
*
|
||||
* Note: "/admin" always requires authentication.
|
||||
*/
|
||||
"requireAuthentication": false,
|
||||
|
||||
/*
|
||||
* Require authorization by a module, or a user with is_admin set, see below.
|
||||
*/
|
||||
"requireAuthorization": false,
|
||||
|
||||
/*
|
||||
* When you use NGINX or another proxy/load-balancer set this to true.
|
||||
*
|
||||
* This is especially necessary when the reverse proxy performs SSL
|
||||
* termination, otherwise the cookies will not have the "secure" flag.
|
||||
*
|
||||
* The other effect will be that the logs will contain the real client's IP,
|
||||
* instead of the reverse proxy's IP.
|
||||
*/
|
||||
"trustProxy": false,
|
||||
|
||||
/*
|
||||
* Settings controlling the session cookie issued by Etherpad.
|
||||
*/
|
||||
"cookie": {
|
||||
/*
|
||||
* How often (in milliseconds) the key used to sign the express_sid cookie
|
||||
* should be rotated. Long rotation intervals reduce signature verification
|
||||
* overhead (because there are fewer historical keys to check) and database
|
||||
* load (fewer historical keys to store, and less frequent queries to
|
||||
* get/update the keys). Short rotation intervals are slightly more secure.
|
||||
*
|
||||
* Multiple Etherpad processes sharing the same database (table) is
|
||||
* supported as long as the clock sync error is significantly less than this
|
||||
* value.
|
||||
*
|
||||
* Key rotation can be disabled (not recommended) by setting this to 0 or
|
||||
* null, or by disabling session expiration (see sessionLifetime).
|
||||
*/
|
||||
"keyRotationInterval": 86400000, // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s
|
||||
|
||||
/*
|
||||
* Value of the SameSite cookie property. "Lax" is recommended unless
|
||||
* Etherpad will be embedded in an iframe from another site, in which case
|
||||
* this must be set to "None". Note: "None" will not work (the browser will
|
||||
* not send the cookie to Etherpad) unless https is used to access Etherpad
|
||||
* (either directly or via a reverse proxy with "trustProxy" set to true).
|
||||
*
|
||||
* "Strict" is not recommended because it has few security benefits but
|
||||
* significant usability drawbacks vs. "Lax". See
|
||||
* https://stackoverflow.com/q/41841880 for discussion.
|
||||
*/
|
||||
"sameSite": "Lax",
|
||||
|
||||
/*
|
||||
* How long (in milliseconds) after navigating away from Etherpad before the
|
||||
* user is required to log in again. (The express_sid cookie is set to
|
||||
* expire at time now + sessionLifetime when first created, and its
|
||||
* expiration time is periodically refreshed to a new now + sessionLifetime
|
||||
* value.) If requireAuthentication is false then this value does not really
|
||||
* matter.
|
||||
*
|
||||
* The "best" value depends on your users' usage patterns and the amount of
|
||||
* convenience you desire. A long lifetime is more convenient (users won't
|
||||
* have to log back in as often) but has some drawbacks:
|
||||
* - It increases the amount of state kept in the database.
|
||||
* - It might weaken security somewhat: The cookie expiration is refreshed
|
||||
* indefinitely without consulting authentication or authorization
|
||||
* hooks, so once a user has accessed a pad, the user can continue to
|
||||
* use the pad until the user leaves for longer than sessionLifetime.
|
||||
* - More historical keys (sessionLifetime / keyRotationInterval) must be
|
||||
* checked when verifying signatures.
|
||||
*
|
||||
* Session lifetime can be set to infinity (not recommended) by setting this
|
||||
* to null or 0. Note that if the session does not expire, most browsers
|
||||
* will delete the cookie when the browser exits, but a session record is
|
||||
* kept in the database forever.
|
||||
*/
|
||||
"sessionLifetime": 864000000, // = 10d * 24h/d * 60m/h * 60s/m * 1000ms/s
|
||||
|
||||
/*
|
||||
* How long (in milliseconds) before the expiration time of an active user's
|
||||
* session is refreshed (to now + sessionLifetime). This setting affects the
|
||||
* following:
|
||||
* - How often a new session expiration time will be written to the
|
||||
* database.
|
||||
* - How often each user's browser will ping the Etherpad server to
|
||||
* refresh the expiration time of the session cookie.
|
||||
*
|
||||
* High values reduce the load on the database and the load from browsers,
|
||||
* but can shorten the effective session lifetime if Etherpad is restarted
|
||||
* or the user navigates away.
|
||||
*
|
||||
* Automatic session refreshes can be disabled (not recommended) by setting
|
||||
* this to null.
|
||||
*/
|
||||
"sessionRefreshInterval": 86400000 // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s
|
||||
},
|
||||
|
||||
/*
|
||||
* Privacy: disable IP logging
|
||||
*/
|
||||
"disableIPlogging": false,
|
||||
|
||||
/*
|
||||
* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
|
||||
* message is shown to user.
|
||||
*
|
||||
* Set to 0 to disable automatic reconnection.
|
||||
*/
|
||||
"automaticReconnectionTimeout": 0,
|
||||
|
||||
/*
|
||||
* By default, when caret is moved out of viewport, it scrolls the minimum
|
||||
* height needed to make this line visible.
|
||||
*/
|
||||
"scrollWhenFocusLineIsOutOfViewport": {
|
||||
|
||||
/*
|
||||
* Percentage of viewport height to be additionally scrolled.
|
||||
*
|
||||
* E.g.: use "percentage.editionAboveViewport": 0.5, to place caret line in
|
||||
* the middle of viewport, when user edits a line above of the
|
||||
* viewport
|
||||
*
|
||||
* Set to 0 to disable extra scrolling
|
||||
*/
|
||||
"percentage": {
|
||||
"editionAboveViewport": 0,
|
||||
"editionBelowViewport": 0
|
||||
},
|
||||
|
||||
/*
|
||||
* Time (in milliseconds) used to animate the scroll transition.
|
||||
* Set to 0 to disable animation
|
||||
*/
|
||||
"duration": 0,
|
||||
|
||||
/*
|
||||
* Flag to control if it should scroll when user places the caret in the
|
||||
* last line of the viewport
|
||||
*/
|
||||
"scrollWhenCaretIsInTheLastLineOfViewport": false,
|
||||
|
||||
/*
|
||||
* Percentage of viewport height to be additionally scrolled when user
|
||||
* presses arrow up in the line of the top of the viewport.
|
||||
*
|
||||
* Set to 0 to let the scroll to be handled as default by Etherpad
|
||||
*/
|
||||
"percentageToScrollWhenUserPressesArrowUp": 0
|
||||
},
|
||||
|
||||
/*
|
||||
* User accounts. These accounts are used by:
|
||||
* - default HTTP basic authentication if no plugin handles authentication
|
||||
* - some but not all authentication plugins
|
||||
* - some but not all authorization plugins
|
||||
*
|
||||
* User properties:
|
||||
* - password: The user's password. Some authentication plugins will ignore
|
||||
* this.
|
||||
* - is_admin: true gives access to /admin. Defaults to false. If you do not
|
||||
* uncomment this, /admin will not be available!
|
||||
* - readOnly: If true, this user will not be able to create new pads or
|
||||
* modify existing pads. Defaults to false.
|
||||
* - canCreate: If this is true and readOnly is false, this user can create
|
||||
* new pads. Defaults to true.
|
||||
*
|
||||
* Authentication and authorization plugins may define additional properties.
|
||||
*
|
||||
* WARNING: passwords should not be stored in plaintext in this file.
|
||||
* If you want to mitigate this, please install ep_hash_auth and
|
||||
* follow the section "secure your installation" in README.md
|
||||
*/
|
||||
|
||||
|
||||
"users": {
|
||||
"admin": {
|
||||
// 1) "password" can be replaced with "hash" if you install ep_hash_auth
|
||||
// 2) please note that if password is null, the user will not be created
|
||||
"password": "changeme1",
|
||||
"is_admin": true
|
||||
},
|
||||
"user": {
|
||||
// 1) "password" can be replaced with "hash" if you install ep_hash_auth
|
||||
// 2) please note that if password is null, the user will not be created
|
||||
"password": "changeme1",
|
||||
"is_admin": false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Restrict socket.io transport methods
|
||||
*/
|
||||
"socketTransportProtocols" : ["websocket", "polling"],
|
||||
|
||||
"socketIo": {
|
||||
/*
|
||||
* Maximum permitted client message size (in bytes). All messages from
|
||||
* clients that are larger than this will be rejected. Large values make it
|
||||
* possible to paste large amounts of text, and plugins may require a larger
|
||||
* value to work properly, but increasing the value increases susceptibility
|
||||
* to denial of service attacks (malicious clients can exhaust memory).
|
||||
*/
|
||||
"maxHttpBufferSize": 1000000
|
||||
},
|
||||
|
||||
/*
|
||||
* Allow Load Testing tools to hit the Etherpad Instance.
|
||||
*
|
||||
* WARNING: this will disable security on the instance.
|
||||
*/
|
||||
"loadTest": false,
|
||||
|
||||
/**
|
||||
* Disable dump of objects preventing a clean exit
|
||||
*/
|
||||
"dumpOnUncleanExit": false,
|
||||
|
||||
/*
|
||||
* Disable indentation on new line when previous line ends with some special
|
||||
* chars (':', '[', '(', '{')
|
||||
*/
|
||||
|
||||
/*
|
||||
"indentationOnNewLine": false,
|
||||
*/
|
||||
|
||||
/*
|
||||
* From Etherpad 1.8.3 onwards, import and export of pads is always rate
|
||||
* limited.
|
||||
*
|
||||
* The default is to allow at most 10 requests per IP in a 90 seconds window.
|
||||
* After that the import/export request is rejected.
|
||||
*
|
||||
* See https://github.com/nfriedly/express-rate-limit for more options
|
||||
*/
|
||||
"importExportRateLimiting": {
|
||||
// duration of the rate limit window (milliseconds)
|
||||
"windowMs": 90000,
|
||||
|
||||
// maximum number of requests per IP to allow during the rate limit window
|
||||
"max": 10
|
||||
},
|
||||
|
||||
/*
|
||||
* From Etherpad 1.8.3 onwards, the maximum allowed size for a single imported
|
||||
* file is always bounded.
|
||||
*
|
||||
* File size is specified in bytes. Default is 50 MB.
|
||||
*/
|
||||
"importMaxFileSize": 52428800, // 50 * 1024 * 1024
|
||||
|
||||
/*
|
||||
* From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited
|
||||
*
|
||||
* The default is to allow at most 10 changes per IP in a 1 second window.
|
||||
* After that the change is rejected.
|
||||
*
|
||||
* See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options
|
||||
*/
|
||||
"commitRateLimiting": {
|
||||
// duration of the rate limit window (seconds)
|
||||
"duration": 1,
|
||||
|
||||
// maximum number of changes per IP to allow during the rate limit window
|
||||
"points": 10
|
||||
},
|
||||
|
||||
/*
|
||||
* Toolbar buttons configuration.
|
||||
*
|
||||
* Uncomment to customize.
|
||||
*/
|
||||
|
||||
/*
|
||||
"toolbar": {
|
||||
"left": [
|
||||
["bold", "italic", "underline", "strikethrough"],
|
||||
["orderedlist", "unorderedlist", "indent", "outdent"],
|
||||
["undo", "redo"],
|
||||
["clearauthorship"]
|
||||
],
|
||||
"right": [
|
||||
["importexport", "timeslider", "savedrevision"],
|
||||
["settings", "embed"],
|
||||
["showusers"]
|
||||
],
|
||||
"timeslider": [
|
||||
["timeslider_export", "timeslider_returnToPad"]
|
||||
]
|
||||
},
|
||||
*/
|
||||
|
||||
/*
|
||||
* Expose Etherpad version in the web interface and in the Server http header.
|
||||
*
|
||||
* Do not enable on production machines.
|
||||
*/
|
||||
"exposeVersion": false,
|
||||
|
||||
/*
|
||||
* The log level we are using.
|
||||
*
|
||||
* Valid values: DEBUG, INFO, WARN, ERROR
|
||||
*/
|
||||
"loglevel": "INFO",
|
||||
|
||||
/* Override any strings found in locale directories */
|
||||
"customLocaleStrings": {},
|
||||
|
||||
/* Disable Admin UI tests */
|
||||
"enableAdminUITests": true,
|
||||
|
||||
/*
|
||||
* Enable/Disable case-insensitive pad names.
|
||||
*/
|
||||
"lowerCasePadIds": false
|
||||
}
|
Loading…
Reference in a new issue