Added playwright tests.

This commit is contained in:
SamTV12345 2024-03-10 14:26:12 +01:00
parent db46ffb63b
commit ca3d621678
15 changed files with 490 additions and 270 deletions

1
.gitignore vendored
View file

@ -25,3 +25,4 @@ out/
plugin_packages
pnpm-lock.yaml
/src/templates/admin
/src/test-results

View file

@ -95,6 +95,7 @@
"mocha-froth": "^0.2.10",
"nodeify": "^1.0.1",
"openapi-schema-validation": "^0.4.2",
"@playwright/test": "^1.42.1",
"selenium-webdriver": "^4.18.1",
"set-cookie-parser": "^2.6.0",
"sinon": "^17.0.1",
@ -118,7 +119,9 @@
"dev": "node --import tsx node/server.ts",
"prod": "node --import tsx node/server.ts",
"ts-check": "tsc --noEmit",
"ts-check:watch": "tsc --noEmit --watch"
"ts-check:watch": "tsc --noEmit --watch",
"test-ui": "npx playwright test tests/frontend-new/specs",
"test-ui:ui": "npx playwright test tests/frontend-new/specs --ui"
},
"version": "1.9.7",
"license": "Apache-2.0"

73
src/playwright.config.ts Normal file
View file

@ -0,0 +1,73 @@
import {defineConfig, devices, test} from '@playwright/test';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/frontend-new/specs',
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. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
baseURL: "localhost:9001",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View file

@ -0,0 +1,127 @@
import {Frame, Locator, Page} from "@playwright/test";
import {MapArrayType} from "../../../node/types/MapType";
import {randomInt} from "node:crypto";
export const getPadOuter = async (page: Page): Promise<Frame> => {
return page.frame('ace_outer')!;
}
export const getPadBody = async (page: Page): Promise<Locator> => {
return page.frame('ace_inner')!.locator('#innerdocbody')
}
export const selectAllText = async (page: Page) => {
await page.keyboard.down('Control');
await page.keyboard.press('A');
await page.keyboard.up('Control');
}
export const toggleUserList = async (page: Page) => {
await page.locator("button[data-l10n-id='pad.toolbar.showusers.title']").click()
}
export const setUserName = async (page: Page, userName: string) => {
await page.waitForSelector('[class="popup popup-show"]')
await page.click("input[data-l10n-id='pad.userlist.entername']");
await page.keyboard.type(userName);
}
export const showChat = async (page: Page) => {
const chatIcon = page.locator("#chaticon")
const classes = await chatIcon.getAttribute('class')
if (classes && !classes.includes('visible')) return
await chatIcon.click()
await page.waitForFunction(`!document.querySelector('#chaticon').classList.contains('visible')`)
}
export const getCurrentChatMessageCount = async (page: Page) => {
return await page.locator('#chattext').locator('p').count()
}
export const getChatUserName = async (page: Page) => {
return await page.locator('#chattext')
.locator('p')
.locator('b')
.innerText()
}
export const getChatMessage = async (page: Page) => {
return (await page.locator('#chattext')
.locator('p')
.textContent({}))!
.split(await getChatTime(page))[1]
}
export const getChatTime = async (page: Page) => {
return await page.locator('#chattext')
.locator('p')
.locator('.time')
.innerText()
}
export const sendChatMessage = async (page: Page, message: string) => {
let currentChatCount = await getCurrentChatMessageCount(page)
const chatInput = page.locator('#chatinput')
await chatInput.click()
await page.keyboard.type(message)
await page.keyboard.press('Enter')
if(message === "") return
await page.waitForFunction(`document.querySelector('#chattext').querySelectorAll('p').length >${currentChatCount}`)
}
export const isChatBoxShown = async (page: Page):Promise<boolean> => {
const classes = await page.locator('#chatbox').getAttribute('class')
return classes !==null && classes.includes('visible')
}
export const isChatBoxSticky = async (page: Page):Promise<boolean> => {
const classes = await page.locator('#chatbox').getAttribute('class')
console.log('Chat', classes && classes.includes('stickyChat'))
return classes !==null && classes.includes('stickyChat')
}
export const hideChat = async (page: Page) => {
if(!await isChatBoxShown(page)|| await isChatBoxSticky(page)) return
await page.locator('#titlecross').click()
await page.waitForFunction(`!document.querySelector('#chatbox').classList.contains('stickyChat')`)
}
export const enableStickyChatviaIcon = async (page: Page) => {
if(await isChatBoxSticky(page)) return
await page.locator('#titlesticky').click()
await page.waitForFunction(`document.querySelector('#chatbox').classList.contains('stickyChat')`)
}
export const disableStickyChatviaIcon = async (page: Page) => {
if(!await isChatBoxSticky(page)) return
await page.locator('#titlecross').click()
await page.waitForFunction(`!document.querySelector('#chatbox').classList.contains('stickyChat')`)
}
export const appendQueryParams = async (page: Page, queryParameters: MapArrayType<string>) => {
const searchParams = new URLSearchParams(page.url().split('?')[1]);
Object.keys(queryParameters).forEach((key) => {
searchParams.append(key, queryParameters[key]);
});
await page.goto(page.url()+"?"+ searchParams.toString());
await page.waitForSelector('iframe[name="ace_outer"]');
}
export const goToNewPad = async (page: Page) => {
// create a new pad before each test run
await page.goto('http://localhost:9001/p/'+"FRONTEND_TESTS"+randomInt(0, 1000));
await page.waitForSelector('iframe[name="ace_outer"]');
}
export const clearPadContent = async (page: Page) => {
await page.keyboard.down('Control');
await page.keyboard.press('A');
await page.keyboard.up('Control');
await page.keyboard.press('Delete');
}

View file

@ -0,0 +1,35 @@
import {Page} from "@playwright/test";
export const isSettingsShown = async (page: Page) => {
const classes = await page.locator('#settings').getAttribute('class')
return classes && classes.includes('popup-show')
}
export const showSettings = async (page: Page) => {
if(await isSettingsShown(page)) return
await page.locator("button[data-l10n-id='pad.toolbar.settings.title']").click()
await page.waitForFunction(`document.querySelector('#settings').classList.contains('popup-show')`)
}
export const hideSettings = async (page: Page) => {
if(!await isSettingsShown(page)) return
await page.locator("button[data-l10n-id='pad.toolbar.settings.title']").click()
await page.waitForFunction(`!document.querySelector('#settings').classList.contains('popup-show')`)
}
export const enableStickyChatviaSettings = async (page: Page) => {
const stickyChat = page.locator('#options-stickychat')
const checked = await stickyChat.isChecked()
if(checked) return
await stickyChat.check({force: true})
await page.waitForSelector('#options-stickychat:checked')
}
export const disableStickyChat = async (page: Page) => {
const stickyChat = page.locator('#options-stickychat')
const checked = await stickyChat.isChecked()
if(!checked) return
await stickyChat.uncheck({force: true})
await page.waitForSelector('#options-stickychat:not(:checked)')
}

View file

@ -0,0 +1,27 @@
import {expect, Page, test} from "@playwright/test";
import {clearPadContent, getPadBody, getPadOuter, goToNewPad} from "../helper/padHelper";
test.beforeEach(async ({ page })=>{
// create a new pad before each test run
await goToNewPad(page);
})
test.describe('All the alphabet works n stuff', () => {
const expectedString = 'abcdefghijklmnopqrstuvwxyz';
test('when you enter any char it appears right', async ({page}) => {
// get the inner iframe
const innerFrame = await getPadBody(page!);
await innerFrame.click();
// delete possible old content
await clearPadContent(page!);
await page.keyboard.type(expectedString);
const text = await innerFrame.locator('div').innerText();
expect(text).toBe(expectedString);
});
});

View file

@ -0,0 +1,50 @@
import {expect, test} from "@playwright/test";
import {randomInt} from "node:crypto";
import {getPadBody, goToNewPad, selectAllText} from "../helper/padHelper";
import exp from "node:constants";
test.beforeEach(async ({ page })=>{
await goToNewPad(page);
})
test.describe('bold button', ()=>{
test('makes text bold on click', async ({page}) => {
// get the inner iframe
const innerFrame = await getPadBody(page);
await innerFrame.click()
// Select pad text
await selectAllText(page);
await page.keyboard.type("Hi Etherpad");
await selectAllText(page);
// click the bold button
await page.locator("button[data-l10n-id='pad.toolbar.bold.title']").click();
// check if the text is bold
expect(await innerFrame.locator('b').innerText()).toBe('Hi Etherpad');
})
test('makes text bold on keypress', async ({page}) => {
// get the inner iframe
const innerFrame = await getPadBody(page);
await innerFrame.click()
// Select pad text
await selectAllText(page);
await page.keyboard.type("Hi Etherpad");
await selectAllText(page);
// Press CTRL + B
await page.keyboard.down('Control');
await page.keyboard.press('b');
await page.keyboard.up('Control');
// check if the text is bold
expect(await innerFrame.locator('b').innerText()).toBe('Hi Etherpad');
})
})

View file

@ -0,0 +1,35 @@
import {expect, test} from "@playwright/test";
import {randomInt} from "node:crypto";
import {goToNewPad, sendChatMessage, setUserName, showChat, toggleUserList} from "../helper/padHelper";
test.beforeEach(async ({ page })=>{
// create a new pad before each test run
await goToNewPad(page);
})
test("Remembers the username after a refresh", async ({page}) => {
await toggleUserList(page);
await setUserName(page,'😃')
await toggleUserList(page)
await page.reload();
await toggleUserList(page);
const usernameField = page.locator("input[data-l10n-id='pad.userlist.entername']");
await expect(usernameField).toHaveValue('😃');
})
test('Own user name is shown when you enter a chat', async ({page})=> {
const chatMessage = 'O hi';
await toggleUserList(page);
await setUserName(page,'😃');
await toggleUserList(page);
await showChat(page);
await sendChatMessage(page,chatMessage);
const chatText = await page.locator('#chattext').locator('p').innerText();
expect(chatText).toContain('😃')
expect(chatText).toContain(chatMessage)
});

View file

@ -0,0 +1,116 @@
import {expect, test} from "@playwright/test";
import {randomInt} from "node:crypto";
import {
appendQueryParams,
disableStickyChatviaIcon,
enableStickyChatviaIcon,
getChatMessage,
getChatTime,
getChatUserName,
getCurrentChatMessageCount, goToNewPad, hideChat, isChatBoxShown, isChatBoxSticky,
sendChatMessage,
showChat,
} from "../helper/padHelper";
import {disableStickyChat, enableStickyChatviaSettings, hideSettings, showSettings} from "../helper/settingsHelper";
test.beforeEach(async ({ page })=>{
await goToNewPad(page);
})
test('opens chat, sends a message, makes sure it exists on the page and hides chat', async ({page}) => {
const chatValue = "JohnMcLear"
// Open chat
await showChat(page);
await sendChatMessage(page, chatValue);
expect(await getCurrentChatMessageCount(page)).toBe(1);
const username = await getChatUserName(page)
const time = await getChatTime(page)
const chatMessage = await getChatMessage(page)
expect(username).toBe('unnamed:');
const regex = new RegExp('^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$');
expect(time).toMatch(regex);
expect(chatMessage).toBe(" "+chatValue);
})
test("makes sure that an empty message can't be sent", async function ({page}) {
const chatValue = 'mluto';
await showChat(page);
await sendChatMessage(page,"");
// Send a message
await sendChatMessage(page,chatValue);
expect(await getCurrentChatMessageCount(page)).toBe(1);
// check that the received message is not the empty one
const username = await getChatUserName(page)
const time = await getChatTime(page);
const chatMessage = await getChatMessage(page);
expect(username).toBe('unnamed:');
const regex = new RegExp('^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$');
expect(time).toMatch(regex);
expect(chatMessage).toBe(" "+chatValue);
});
test('makes chat stick to right side of the screen via settings, remove sticky via settings, close it', async ({page}) =>{
await showSettings(page);
await enableStickyChatviaSettings(page);
expect(await isChatBoxShown(page)).toBe(true);
expect(await isChatBoxSticky(page)).toBe(true);
await disableStickyChat(page);
expect(await isChatBoxShown(page)).toBe(true);
expect(await isChatBoxSticky(page)).toBe(false);
await hideSettings(page);
await hideChat(page);
expect(await isChatBoxShown(page)).toBe(false);
expect(await isChatBoxSticky(page)).toBe(false);
});
test('makes chat stick to right side of the screen via icon on the top right, ' +
'remove sticky via icon, close it', async function ({page}) {
await showChat(page);
await enableStickyChatviaIcon(page);
expect(await isChatBoxShown(page)).toBe(true);
expect(await isChatBoxSticky(page)).toBe(true);
await disableStickyChatviaIcon(page);
expect(await isChatBoxShown(page)).toBe(true);
expect(await isChatBoxSticky(page)).toBe(false);
await hideChat(page);
expect(await isChatBoxSticky(page)).toBe(false);
expect(await isChatBoxShown(page)).toBe(false);
});
test('Checks showChat=false URL Parameter hides chat then' +
' when removed it shows chat', async function ({page}) {
// get a new pad, but don't clear the cookies
await appendQueryParams(page, {
showChat: 'false'
});
const chaticon = page.locator('#chaticon')
// chat should be hidden.
expect(await chaticon.isVisible()).toBe(false);
// get a new pad, but don't clear the cookies
await goToNewPad(page);
const secondChatIcon = page.locator('#chaticon')
// chat should be visible.
expect(await secondChatIcon.isVisible()).toBe(true)
});

View file

@ -0,0 +1,22 @@
import {expect, test} from "@playwright/test";
import {clearPadContent, getPadBody, goToNewPad} from "../helper/padHelper";
test.beforeEach(async ({ page })=>{
// create a new pad before each test run
await goToNewPad(page);
})
test('delete keystroke', async ({page}) => {
const padText = "Hello World this is a test"
const body = await getPadBody(page)
await body.click()
await clearPadContent(page)
await page.keyboard.type(padText)
// Navigate to the end of the text
await page.keyboard.press('End');
// Delete the last character
await page.keyboard.press('Backspace');
const text = await body.locator('div').innerText();
expect(text).toBe(padText.slice(0, -1));
})

View file

@ -1,24 +0,0 @@
'use strict';
describe('All the alphabet works n stuff', function () {
const expectedString = 'abcdefghijklmnopqrstuvwxyz';
// create a new pad before each test run
beforeEach(async function () {
await helper.aNewPad();
});
it('when you enter any char it appears right', function (done) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const firstTextElement = inner$('div').first();
// simulate key presses to delete content
firstTextElement.sendkeys('{selectall}'); // select all
firstTextElement.sendkeys('{del}'); // clear the first line
firstTextElement.sendkeys(expectedString); // insert the string
helper.waitFor(() => inner$('div').first().text() === expectedString, 2000).done(done);
});
});

View file

@ -1,64 +0,0 @@
'use strict';
describe('bold button', function () {
// create a new pad before each test run
beforeEach(async function () {
await helper.aNewPad();
});
it('makes text bold on click', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
// get the bold button and click it
const $boldButton = chrome$('.buttonicon-bold');
$boldButton.trigger('click');
const $newFirstTextElement = inner$('div').first();
// is there a <b> element now?
const isBold = $newFirstTextElement.find('b').length === 1;
// expect it to be bold
expect(isBold).to.be(true);
// make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
it('makes text bold on keypress', function (done) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key
e.which = 66; // b
inner$('#innerdocbody').trigger(e);
const $newFirstTextElement = inner$('div').first();
// is there a <b> element now?
const isBold = $newFirstTextElement.find('b').length === 1;
// expect it to be bold
expect(isBold).to.be(true);
// make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -1,35 +0,0 @@
'use strict';
describe('change username value', function () {
// create a new pad before each test run
beforeEach(async function () {
await helper.aNewPad();
});
it('Remembers the user name after a refresh', async function () {
this.timeout(10000);
await helper.toggleUserList();
await helper.setUserName('😃');
// Give the server an opportunity to write the new name.
await new Promise((resolve) => setTimeout(resolve, 1000));
// get a new pad, but don't clear the cookies
await helper.aNewPad({clearCookies: false});
await helper.toggleUserList();
await helper.waitForPromise(() => helper.usernameField().val() === '😃');
});
it('Own user name is shown when you enter a chat', async function () {
this.timeout(10000);
await helper.toggleUserList();
await helper.setUserName('😃');
await helper.showChat();
await helper.sendChatMessage('O hi{enter}');
await helper.waitForPromise(() => {
// username:hours:minutes text
const chatText = helper.chatTextParagraphs().text();
return chatText.indexOf('😃') === 0;
});
});
});

View file

@ -1,116 +0,0 @@
'use strict';
describe('Chat messages and UI', function () {
// create a new pad before each test run
beforeEach(async function () {
await helper.aNewPad();
});
it('opens chat, sends a message, makes sure it exists ' +
'on the page and hides chat', async function () {
this.timeout(3000);
const chatValue = 'JohnMcLear';
await helper.showChat();
await helper.sendChatMessage(`${chatValue}{enter}`);
expect(helper.chatTextParagraphs().length).to.be(1);
// <p data-authorid="a.qjkwNs4z0pPROphS"
// class="author-a-qjkwz78zs4z122z0pz80zz82zz79zphz83z">
// <b>unnamed:</b>
// <span class="time author-a-qjkwz78zs4z122z0pz80zz82zz79zphz83z">12:38
// </span> JohnMcLear
// </p>
const username = helper.chatTextParagraphs().children('b').text();
const time = helper.chatTextParagraphs().children('.time').text();
// TODO: The '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of it
// without breaking the other tests that use $.sendkeys().
expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}\n`);
await helper.hideChat();
});
it("makes sure that an empty message can't be sent", async function () {
const chatValue = 'mluto';
await helper.showChat();
// simulate a keypress of typing enter, mluto and enter (to send 'mluto')
await helper.sendChatMessage(`{enter}${chatValue}{enter}`);
const chat = helper.chatTextParagraphs();
expect(chat.length).to.be(1);
// check that the received message is not the empty one
const username = chat.children('b').text();
const time = chat.children('.time').text();
// TODO: Each '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of them
// without breaking the other tests that use $.sendkeys().
expect(chat.text()).to.be(`${username}${time} \n${chatValue}\n`);
});
it('makes chat stick to right side of the screen via settings, ' +
'remove sticky via settings, close it', async function () {
this.timeout(5000);
await helper.showSettings();
await helper.enableStickyChatviaSettings();
expect(helper.isChatboxShown()).to.be(true);
expect(helper.isChatboxSticky()).to.be(true);
await helper.disableStickyChatviaSettings();
expect(helper.isChatboxSticky()).to.be(false);
expect(helper.isChatboxShown()).to.be(true);
await helper.hideChat();
expect(helper.isChatboxSticky()).to.be(false);
expect(helper.isChatboxShown()).to.be(false);
});
it('makes chat stick to right side of the screen via icon on the top' +
' right, remove sticky via icon, close it', async function () {
this.timeout(5000);
await helper.showChat();
await helper.enableStickyChatviaIcon();
expect(helper.isChatboxShown()).to.be(true);
expect(helper.isChatboxSticky()).to.be(true);
await helper.disableStickyChatviaIcon();
expect(helper.isChatboxShown()).to.be(true);
expect(helper.isChatboxSticky()).to.be(false);
await helper.hideChat();
expect(helper.isChatboxSticky()).to.be(false);
expect(helper.isChatboxShown()).to.be(false);
});
xit('Checks showChat=false URL Parameter hides chat then' +
' when removed it shows chat', async function () {
// give it a second to save the username on the server side
await new Promise((resolve) => setTimeout(resolve, 3000));
// get a new pad, but don't clear the cookies
await helper.aNewPad({clearCookies: false, params: {showChat: 'false'}});
let chrome$ = helper.padChrome$;
let chaticon = chrome$('#chaticon');
// chat should be hidden.
expect(chaticon.is(':visible')).to.be(false);
// give it a second to save the username on the server side
await new Promise((resolve) => setTimeout(resolve, 1000));
// get a new pad, but don't clear the cookies
await helper.aNewPad({clearCookies: false});
chrome$ = helper.padChrome$;
chaticon = chrome$('#chaticon');
// chat should be visible.
expect(chaticon.is(':visible')).to.be(true);
});
});

View file

@ -1,30 +0,0 @@
'use strict';
describe('delete keystroke', function () {
// create a new pad before each test run
beforeEach(async function () {
await helper.aNewPad();
});
it('makes text delete', async function () {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// get the original length of this element
const elementLength = $firstTextElement.text().length;
// simulate key presses to delete content
$firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key
$firstTextElement.sendkeys('{del}'); // simulate a keypress of delete
const $newFirstTextElement = inner$('div').first();
// get the new length of this element
const newElementLength = $newFirstTextElement.text().length;
// expect it to be one char less in length
expect(newElementLength).to.be((elementLength - 1));
});
});