From d8ae7cda37d1be11f8d730d52328e0f018cd8970 Mon Sep 17 00:00:00 2001 From: abhijeetgorhe26 Date: Sat, 30 May 2026 02:27:13 +0530 Subject: [PATCH 1/7] fixes #7865 --- admin/src/pages/PadPage.tsx | 2 +- src/static/js/pad_userlist.ts | 2 +- src/static/skins/colibris/index.js | 5 +++-- src/static/skins/colibris/pad.js | 2 +- var/.gitignore | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/admin/src/pages/PadPage.tsx b/admin/src/pages/PadPage.tsx index c216ffab79f..039c10b578d 100644 --- a/admin/src/pages/PadPage.tsx +++ b/admin/src/pages/PadPage.tsx @@ -418,7 +418,7 @@ export const PadPage = () => { diff --git a/src/static/js/pad_userlist.ts b/src/static/js/pad_userlist.ts index d3ee825276c..7366348a1f0 100644 --- a/src/static/js/pad_userlist.ts +++ b/src/static/js/pad_userlist.ts @@ -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; diff --git a/src/static/skins/colibris/index.js b/src/static/skins/colibris/index.js index f36edf84180..7936cb278ad 100644 --- a/src/static/skins/colibris/index.js +++ b/src/static/skins/colibris/index.js @@ -70,12 +70,13 @@ window.customStart = () => { li.style.cursor = 'pointer'; li.className = 'recent-pad'; - const padPath = `${window.location.href}p/${pad.name}`; + const decodedName = decodeURIComponent(pad.name); + const padPath = `${window.location.href}p/${encodeURIComponent(decodedName)}`; const link = document.createElement('a'); link.style.textDecoration = 'none'; link.href = padPath; - link.innerText = pad.name; + link.innerText = decodedName; li.appendChild(link); diff --git a/src/static/skins/colibris/pad.js b/src/static/skins/colibris/pad.js index 1e7a85b3faa..ce509a8892b 100644 --- a/src/static/skins/colibris/pad.js +++ b/src/static/skins/colibris/pad.js @@ -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([])); diff --git a/var/.gitignore b/var/.gitignore index d75cb9e42b2..578a814a59a 100644 --- a/var/.gitignore +++ b/var/.gitignore @@ -3,3 +3,4 @@ minified* installed_plugins.json dirty.db rusty.db +update-state.json \ No newline at end of file From 9a35fab26eea3a76b61d8d4e13288ab1710e7567 Mon Sep 17 00:00:00 2001 From: abhijeetgorhe26 <121374073+abhijeetgorhe26@users.noreply.github.com> Date: Sat, 30 May 2026 02:38:12 +0530 Subject: [PATCH 2/7] fixes #7865 From 70cd279f10097c31f42e8ae105705b2468cd8f87 Mon Sep 17 00:00:00 2001 From: abhijeetgorhe26 Date: Sat, 30 May 2026 18:02:38 +0530 Subject: [PATCH 3/7] fixes #7865 --- src/static/skins/colibris/index.js | 5 ++- .../frontend-new/specs/recent_pads.spec.ts | 32 +++++++++++++++++++ var/log/update.log | 3 ++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/tests/frontend-new/specs/recent_pads.spec.ts create mode 100644 var/log/update.log diff --git a/src/static/skins/colibris/index.js b/src/static/skins/colibris/index.js index 7936cb278ad..3da389330bc 100644 --- a/src/static/skins/colibris/index.js +++ b/src/static/skins/colibris/index.js @@ -70,13 +70,12 @@ window.customStart = () => { li.style.cursor = 'pointer'; li.className = 'recent-pad'; - const decodedName = decodeURIComponent(pad.name); - const padPath = `${window.location.href}p/${encodeURIComponent(decodedName)}`; + const padPath = `${window.location.href}p/${encodeURIComponent(pad.name)}`; const link = document.createElement('a'); link.style.textDecoration = 'none'; link.href = padPath; - link.innerText = decodedName; + link.innerText = pad.name; li.appendChild(link); diff --git a/src/tests/frontend-new/specs/recent_pads.spec.ts b/src/tests/frontend-new/specs/recent_pads.spec.ts new file mode 100644 index 00000000000..a73ef83aa76 --- /dev/null +++ b/src/tests/frontend-new/specs/recent_pads.spec.ts @@ -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); + }); +}); diff --git a/var/log/update.log b/var/log/update.log new file mode 100644 index 00000000000..6832f83eeb5 --- /dev/null +++ b/var/log/update.log @@ -0,0 +1,3 @@ +[2026-05-30T12:28:01.272Z] CANCEL by admin during status=scheduled +[2026-05-30T12:28:01.289Z] ACKNOWLEDGE rollback-failed -> idle +[2026-05-30T12:28:01.303Z] ACKNOWLEDGE preflight-failed -> idle From e2acf22b6e8d82fb18e43bce4f2ea25a6d9837b0 Mon Sep 17 00:00:00 2001 From: abhijeetgorhe26 <121374073+abhijeetgorhe26@users.noreply.github.com> Date: Sat, 30 May 2026 18:10:24 +0530 Subject: [PATCH 4/7] fixes #7865 --- var/.gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/var/.gitignore b/var/.gitignore index 578a814a59a..d75cb9e42b2 100644 --- a/var/.gitignore +++ b/var/.gitignore @@ -3,4 +3,3 @@ minified* installed_plugins.json dirty.db rusty.db -update-state.json \ No newline at end of file From 78de447f078cbec3b782061465a177b562956630 Mon Sep 17 00:00:00 2001 From: abhijeetgorhe26 <121374073+abhijeetgorhe26@users.noreply.github.com> Date: Sat, 30 May 2026 20:38:26 +0530 Subject: [PATCH 5/7] Fixes #7865 --- var/log/update.log | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 var/log/update.log diff --git a/var/log/update.log b/var/log/update.log deleted file mode 100644 index 6832f83eeb5..00000000000 --- a/var/log/update.log +++ /dev/null @@ -1,3 +0,0 @@ -[2026-05-30T12:28:01.272Z] CANCEL by admin during status=scheduled -[2026-05-30T12:28:01.289Z] ACKNOWLEDGE rollback-failed -> idle -[2026-05-30T12:28:01.303Z] ACKNOWLEDGE preflight-failed -> idle From 7ab1cc460bba47c54eac40abebf5b67f88363555 Mon Sep 17 00:00:00 2001 From: abhijeetgorhe26 Date: Sat, 30 May 2026 21:25:36 +0530 Subject: [PATCH 6/7] fix CI: correct upgrade workflow pnpm install order --- .github/workflows/upgrade-from-latest-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index c3ef545f519..3ff00f57547 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -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 @@ -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 From d800465962fe774939e6c45ab2993962ec2375f2 Mon Sep 17 00:00:00 2001 From: abhijeetgorhe26 Date: Tue, 2 Jun 2026 01:30:16 +0530 Subject: [PATCH 7/7] Added Test for share dialog (#7865) --- .../frontend-new/specs/embed_value.spec.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/tests/frontend-new/specs/embed_value.spec.ts b/src/tests/frontend-new/specs/embed_value.spec.ts index c4abe8201ba..0d8ca2de438 100644 --- a/src/tests/frontend-new/specs/embed_value.spec.ts +++ b/src/tests/frontend-new/specs/embed_value.spec.ts @@ -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); + }); + }); })