Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/upgrade-from-latest-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,6 @@ jobs:
-
name: Run the backend tests
run: pnpm run test
-
name: Install all dependencies and symlink for ep_etherpad-lite
run: pnpm install --frozen-lockfile
# The job starts from the latest release tag, so fetch the pull-request
# ref explicitly before checking out ${GITHUB_SHA}. A plain "git fetch"
# only brings "normal" references (refs/heads/* and refs/tags/*), and for
Expand All @@ -99,5 +96,8 @@ jobs:
# For pull requests, ${GITHUB_SHA} is the automatically generated merge
# commit that merges the PR's source branch to its destination branch.
run: git checkout "${GITHUB_SHA}"
-
name: Install all dependencies and symlink for ep_etherpad-lite
run: pnpm install --frozen-lockfile
- name: Run the backend tests
run: pnpm run test
2 changes: 1 addition & 1 deletion admin/src/pages/PadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ export const PadPage = () => {
</button>
<button
className="pm-btn pm-btn-primary pm-btn--sm"
onClick={() => window.open(`../../p/${pad.padName}`, '_blank')}
onClick={() => window.open(`../../p/${encodeURIComponent(pad.padName)}`, '_blank')}
>
<Eye size={13}/> <Trans i18nKey="admin_pads.open"/>
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/static/js/pad_userlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ const paduserlist = (() => {
if (localStorage.getItem('recentPads') != null) {
const recentPadsList = JSON.parse(localStorage.getItem('recentPads'));
const pathSegments = window.location.pathname.split('/');
const padName = pathSegments[pathSegments.length - 1];
const padName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
const existingPad = recentPadsList.find((pad) => pad.name === padName);
if (existingPad) {
existingPad.members = online;
Expand Down
2 changes: 1 addition & 1 deletion src/static/skins/colibris/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ window.customStart = () => {
li.style.cursor = 'pointer';

li.className = 'recent-pad';
const padPath = `${window.location.href}p/${pad.name}`;
const padPath = `${window.location.href}p/${encodeURIComponent(pad.name)}`;
const link = document.createElement('a');
link.style.textDecoration = 'none';

Expand Down
2 changes: 1 addition & 1 deletion src/static/skins/colibris/pad.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ window.customStart = () => {
$('.buttonicon').on('mouseup', function () { $(this).parent().removeClass('pressed'); });

const pathSegments = window.location.pathname.split('/');
const padName = pathSegments[pathSegments.length - 1];
const padName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
const recentPads = localStorage.getItem('recentPads');
if (recentPads == null) {
localStorage.setItem('recentPads', JSON.stringify([]));
Expand Down
102 changes: 102 additions & 0 deletions src/tests/frontend-new/specs/embed_value.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,106 @@ test.describe('embed links', function () {
await checkiFrameCode(embedCode, true, page);
});
})

test.describe('UI interactions and accessibility', function () {
test.beforeEach(async ({ page }) => {
await goToNewPad(page);
});

test('focuses the dialog container on open', async function ({page}) {
const shareButton = page.locator('button[data-l10n-id="pad.toolbar.embed.title"]');
await shareButton.click();

const dialog = page.locator('#embed');
await expect(dialog).toBeFocused();
});

test('clicking inside inputs selects the entire text content', async function ({page}) {
const shareButton = page.locator('button[data-l10n-id="pad.toolbar.embed.title"]');
await shareButton.click();

// Focus another element first to clear selection/focus
const embedInput = page.locator('#embedinput');
await embedInput.click();

// Verify embedinput is fully selected on click
let selection = await page.evaluate(() => {
const activeEl = document.activeElement as HTMLInputElement;
return {
id: activeEl?.id,
selectionStart: activeEl?.selectionStart,
selectionEnd: activeEl?.selectionEnd,
valueLength: activeEl?.value.length,
};
});
expect(selection.id).toBe('embedinput');
expect(selection.selectionStart).toBe(0);
expect(selection.selectionEnd).toBe(selection.valueLength);

// Now click linkinput
const linkInput = page.locator('#linkinput');
await linkInput.click();

selection = await page.evaluate(() => {
const activeEl = document.activeElement as HTMLInputElement;
return {
id: activeEl?.id,
selectionStart: activeEl?.selectionStart,
selectionEnd: activeEl?.selectionEnd,
valueLength: activeEl?.value.length,
};
});
expect(selection.id).toBe('linkinput');
expect(selection.selectionStart).toBe(0);
expect(selection.selectionEnd).toBe(selection.valueLength);
});

test('Escape key closes the dialog and restores focus to the trigger', async function ({page}) {
const shareButton = page.locator('button[data-l10n-id="pad.toolbar.embed.title"]');
await shareButton.click();

const dialog = page.locator('#embed');
await expect(dialog).toHaveClass(/popup-show/);
// Wait for focus to land on the dialog to prevent any asynchronous race conditions under load
await expect(dialog).toBeFocused();

await page.keyboard.press('Escape');
await expect(dialog).not.toHaveClass(/popup-show/);

// Verify focus is restored to the share button
const focusedL10nId = await page.evaluate(() => document.activeElement?.getAttribute('data-l10n-id') || '');
expect(focusedL10nId).toBe('pad.toolbar.embed.title');
});

test('bi-directional checkbox toggling updates links accordingly', async function ({page}) {
const shareButton = page.locator('button[data-l10n-id="pad.toolbar.embed.title"]');
await shareButton.click();

const linkInput = page.locator('#linkinput');
const embedInput = page.locator('#embedinput');
const readonlyCheckbox = page.locator('#readonlyinput');

// Unchecked by default: should be read-write
const initialLink = await linkInput.inputValue();
const initialEmbed = await embedInput.inputValue();
expect(initialLink.indexOf('r.') > 0).toBe(false);
expect(initialEmbed.indexOf('r.') > 0).toBe(false);

// Check it -> updates to read-only
await readonlyCheckbox.click({force: true});
await page.waitForSelector('#readonlyinput:checked');
const roLink = await linkInput.inputValue();
const roEmbed = await embedInput.inputValue();
expect(roLink.indexOf('r.') > 0).toBe(true);
expect(roEmbed.indexOf('embed_readonly') > 0).toBe(true);

// Uncheck it -> updates back to read-write
await readonlyCheckbox.click({force: true});
await page.waitForSelector('#readonlyinput:not(:checked)');
const rwLink = await linkInput.inputValue();
const rwEmbed = await embedInput.inputValue();
expect(rwLink).toBe(initialLink);
expect(rwEmbed).toBe(initialEmbed);
});
});
})
32 changes: 32 additions & 0 deletions src/tests/frontend-new/specs/recent_pads.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';

test.describe('Recent Pads', () => {
test('should display correctly encoded URLs for recent pads', async ({ page }) => {
const padName = 'test pad with spaces & / chars';
const recentPads = [
{
name: padName,
timestamp: new Date().toISOString(),
members: 1,
},
];

// Add recent pad to localStorage before navigating
await page.addInitScript((data) => {
window.localStorage.setItem('recentPads', data);
}, JSON.stringify(recentPads));

await page.goto('localhost:9001/');

const recentPad = page.locator('.recent-pad').first();
await expect(recentPad).toBeVisible();

const link = recentPad.locator('a');
await expect(link).toHaveText(padName);

// Assert the href has the properly encoded URL
const expectedEncodedName = encodeURIComponent(padName);
const expectedHrefRegex = new RegExp(`p/${expectedEncodedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
await expect(link).toHaveAttribute('href', expectedHrefRegex);
});
});