From 267b8a212a51c8aac7bcd716097048e28f82c06f Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 12 Jun 2026 11:46:24 +0100 Subject: [PATCH 1/3] test(timeslider): port legacy mocha specs to Playwright, retire orphaned suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy src/tests/frontend/specs/ mocha suite is run by no CI workflow, so its timeslider coverage was dead — a regression in the in-pad history UI (#7659/#7946) sailed through CI. Port the still-meaningful cases to frontend-new Playwright specs, re-targeted at the real in-pad UI (outer banner / slider / export links that pad_mode.ts drives) rather than the isolated ?embed=1 iframe the existing specs use: - timeslider_revision_labels.spec.ts (from timeslider_labels.js): the #history-banner shows 'Version N' + a valid (non-NaN) date and timer, and both update when scrubbing to revision 0. - timeslider_export_links.spec.ts (from timeslider_numeric_padID.js and the 'checks the export url' case of timeslider_revisions.js): the outer export hrefs target /p///export/ for the viewed revision, including a numeric pad id, and follow the slider to revision 0. - timeslider_deeplink.spec.ts (from the 'jumps to a revision given in the url' case): a #rev/N hash — and the legacy #N shortlink form — boots straight into history mode at that revision. Delete the three now-ported legacy specs. Verified passing on Chromium and Firefox. The star-marker case of timeslider_revisions.js is already covered by timeslider_saved_revisions.spec.ts (#7948). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/timeslider_deeplink.spec.ts | 50 ++++++ .../specs/timeslider_export_links.spec.ts | 86 ++++++++++ .../specs/timeslider_revision_labels.spec.ts | 77 +++++++++ src/tests/frontend/specs/timeslider_labels.js | 65 -------- .../specs/timeslider_numeric_padID.js | 31 ---- .../frontend/specs/timeslider_revisions.js | 150 ------------------ 6 files changed, 213 insertions(+), 246 deletions(-) create mode 100644 src/tests/frontend-new/specs/timeslider_deeplink.spec.ts create mode 100644 src/tests/frontend-new/specs/timeslider_export_links.spec.ts create mode 100644 src/tests/frontend-new/specs/timeslider_revision_labels.spec.ts delete mode 100644 src/tests/frontend/specs/timeslider_labels.js delete mode 100644 src/tests/frontend/specs/timeslider_numeric_padID.js delete mode 100644 src/tests/frontend/specs/timeslider_revisions.js diff --git a/src/tests/frontend-new/specs/timeslider_deeplink.spec.ts b/src/tests/frontend-new/specs/timeslider_deeplink.spec.ts new file mode 100644 index 00000000000..143cadebb58 --- /dev/null +++ b/src/tests/frontend-new/specs/timeslider_deeplink.spec.ts @@ -0,0 +1,50 @@ +import {expect, Page, test} from "@playwright/test"; +import {clearPadContent, goToNewPad, writeToPad} from "../helper/padHelper"; + +// Ported from the "jumps to a revision given in the url" case of the legacy +// timeslider_revisions.js (which no CI workflow ran). Re-targeted at the in-pad +// history model (#7659): a #rev/N hash on the pad URL boots straight into +// history mode at that revision (pad_mode.bootstrapFromHash), and the legacy +// #N shortlink form is still accepted for old bookmarks. +test.describe('timeslider deep link', function () { + test.describe.configure({mode: 'serial'}); + + test.beforeEach(async ({context}) => { + await context.clearCookies(); + }); + + const expectHistoryAtRev0 = async (page: Page) => { + await expect(page.locator('body.history-mode')).toBeVisible({timeout: 15000}); + await expect(page.locator('#history-controls')).toBeVisible(); + await expect(page.locator('#history-banner-rev')).toHaveText('Version 0', {timeout: 15000}); + await expect.poll( + async () => await page.locator('#history-slider-input').evaluate( + (el) => Number((el as HTMLInputElement).value)), + {timeout: 15000}).toBe(0); + }; + + test('#rev/N hash boots into history mode at that revision', async function ({page}) { + const padId = await goToNewPad(page); + await clearPadContent(page); + await writeToPad(page, 'One '); + await page.waitForTimeout(400); + await writeToPad(page, 'Two '); + await page.waitForTimeout(800); + + // Deep-link to revision 0 of the same pad. + await page.goto(`http://localhost:9001/p/${padId}#rev/0`); + await expectHistoryAtRev0(page); + }); + + test('legacy #N shortlink hash still enters history mode', async function ({page}) { + const padId = await goToNewPad(page); + await clearPadContent(page); + await writeToPad(page, 'One '); + await page.waitForTimeout(400); + await writeToPad(page, 'Two '); + await page.waitForTimeout(800); + + await page.goto(`http://localhost:9001/p/${padId}#0`); + await expectHistoryAtRev0(page); + }); +}); diff --git a/src/tests/frontend-new/specs/timeslider_export_links.spec.ts b/src/tests/frontend-new/specs/timeslider_export_links.spec.ts new file mode 100644 index 00000000000..4a910aa2dac --- /dev/null +++ b/src/tests/frontend-new/specs/timeslider_export_links.spec.ts @@ -0,0 +1,86 @@ +import {expect, Page, test} from "@playwright/test"; +import {clearPadContent, goToPad, writeToPad} from "../helper/padHelper"; + +// Ported from the legacy mocha suite (timeslider_numeric_padID.js and the +// "checks the export url" case in timeslider_revisions.js), neither of which +// ran in CI. Re-targeted at the in-pad history UI (#7659): the export links +// live in the outer #exportColumn and pad_mode.ts rewrites their hrefs to +// /p///export/ for the revision currently being viewed. +test.describe('timeslider export links', function () { + test.describe.configure({mode: 'serial'}); + + test.beforeEach(async ({context}) => { + await context.clearCookies(); + }); + + // Suppress the one-time pad-deletion-token modal (same trick goToNewPad uses) + // so it can't steal focus mid-test on a creator session. + const suppressDeletionTokenModal = async (page: Page) => { + await page.addInitScript(() => { + let stored: unknown; + Object.defineProperty(window, 'clientVars', { + configurable: true, + get() { return stored; }, + set(v) { + if (v != null && typeof v === 'object') { + (v as {padDeletionToken?: string | null}).padDeletionToken = null; + } + stored = v; + }, + }); + }); + }; + + const enterHistoryMode = async (page: Page) => { + await page.click('.buttonicon-history'); + await page.waitForSelector('#history-controls:not([hidden])', {state: 'visible'}); + await page.waitForSelector('#history-frame'); + }; + + const goToRevision = async (page: Page, rev: number) => { + await page.locator('#history-slider-input').evaluate((el, value) => { + (el as HTMLInputElement).value = String(value); + el.dispatchEvent(new Event('input', {bubbles: true})); + }, rev); + await expect(page.locator('#history-banner-rev')).toHaveText(`Version ${rev}`, {timeout: 15000}); + }; + + const exportHref = (page: Page, id: string) => + page.locator(`#${id}`).getAttribute('href'); + + test('export hrefs target the viewed revision, including a numeric pad id', async function ({page}) { + // A numeric pad id is the specific case the legacy test guarded — the + // href rewriter must not confuse it with the revision segment. + const padId = String(735773577357 + Math.floor(Math.random() * 1000)); + await suppressDeletionTokenModal(page); + await goToPad(page, padId); // navigates and waits for the editor to be ready + await clearPadContent(page); + + await writeToPad(page, 'One '); + await page.waitForTimeout(400); + await writeToPad(page, 'Two '); + await page.waitForTimeout(800); + + await enterHistoryMode(page); + + // Wait for pad_mode to sync the slider max from the embedded BroadcastSlider. + await expect.poll( + async () => await page.locator('#history-slider-input').evaluate( + (el) => Number((el as HTMLInputElement).max)), + {timeout: 15000}).toBeGreaterThan(0); + const maxRev = await page.locator('#history-slider-input').evaluate( + (el) => Number((el as HTMLInputElement).max)); + expect(maxRev).toBeGreaterThan(0); + + // On entry the slider is at the latest revision; hrefs point there. + await expect.poll(() => exportHref(page, 'exporthtmla'), {timeout: 15000}) + .toContain(`/${padId}/${maxRev}/export/html`); + expect(await exportHref(page, 'exportplaina')).toContain(`/${padId}/${maxRev}/export/txt`); + + // Scrub to revision 0 — the export targets must follow. + await goToRevision(page, 0); + await expect.poll(() => exportHref(page, 'exporthtmla'), {timeout: 15000}) + .toContain(`/${padId}/0/export/html`); + expect(await exportHref(page, 'exportplaina')).toContain(`/${padId}/0/export/txt`); + }); +}); diff --git a/src/tests/frontend-new/specs/timeslider_revision_labels.spec.ts b/src/tests/frontend-new/specs/timeslider_revision_labels.spec.ts new file mode 100644 index 00000000000..84bb7884133 --- /dev/null +++ b/src/tests/frontend-new/specs/timeslider_revision_labels.spec.ts @@ -0,0 +1,77 @@ +import {expect, Page, test} from "@playwright/test"; +import {clearPadContent, goToNewPad, writeToPad} from "../helper/padHelper"; + +// Ported from the legacy mocha suite (src/tests/frontend/specs/timeslider_labels.js), +// which no CI workflow ran. Re-targeted at the in-pad history UI (#7659): the +// revision label and date are shown in the outer #history-banner, populated by +// pad_mode.ts mirroring the embedded timeslider's #revision_label / #revision_date. +// This guards the banner bridge against silently breaking again (cf. #7946). +test.describe('timeslider revision labels', function () { + test.describe.configure({mode: 'serial'}); + + test.beforeEach(async ({context}) => { + await context.clearCookies(); + }); + + const enterHistoryMode = async (page: Page) => { + await page.click('.buttonicon-history'); + await page.waitForSelector('#history-controls:not([hidden])', {state: 'visible'}); + await page.waitForSelector('#history-frame'); + }; + + // Drive the outer slider (a remote control for the embedded BroadcastSlider) + // to a specific revision and wait for the banner to reflect it. + const goToRevision = async (page: Page, rev: number) => { + await page.locator('#history-slider-input').evaluate((el, value) => { + (el as HTMLInputElement).value = String(value); + el.dispatchEvent(new Event('input', {bubbles: true})); + }, rev); + await expect(page.locator('#history-banner-rev')).toHaveText(`Version ${rev}`, {timeout: 15000}); + }; + + // "Saved June 12, 2026" -> a parseable Date (the banner mirrors the + // timeslider.saved l10n string "Saved {{month}} {{day}}, {{year}}"). + const parsedBannerDate = async (page: Page) => await page.locator('#history-banner-date').evaluate( + (el) => new Date((el.textContent || '').replace(/^Saved\s+/i, '')).getTime()); + + test('shows Version label and a valid date that update while scrubbing', async function ({page}) { + await goToNewPad(page); + await clearPadContent(page); + + // Produce a few revisions. + await writeToPad(page, 'Alpha '); + await page.waitForTimeout(400); + await writeToPad(page, 'Beta '); + await page.waitForTimeout(400); + await writeToPad(page, 'Gamma '); + await page.waitForTimeout(800); + + await enterHistoryMode(page); + + // On entry the slider sits at the latest revision; the banner must show a + // non-empty "Version N" label and a non-NaN date. Wait for pad_mode to sync + // the slider max from the embedded BroadcastSlider before reading it. + await expect.poll( + async () => await page.locator('#history-slider-input').evaluate( + (el) => Number((el as HTMLInputElement).max)), + {timeout: 15000}).toBeGreaterThan(0); + const maxRev = await page.locator('#history-slider-input').evaluate( + (el) => Number((el as HTMLInputElement).max)); + expect(maxRev).toBeGreaterThan(0); + + await expect(page.locator('#history-banner-rev')).toHaveText(`Version ${maxRev}`); + const dateLast = await parsedBannerDate(page); + expect(Number.isNaN(dateLast)).toBe(false); + // The mirrored timer must also be a real, non-NaN datetime. + const timerLast = await page.locator('#history-timer').textContent(); + expect(Number.isNaN(new Date(timerLast || '').getTime())).toBe(false); + + // Scrub back to revision 0 — label and date must update. + await goToRevision(page, 0); + await expect(page.locator('#history-banner-rev')).toHaveText('Version 0'); + const dateFirst = await parsedBannerDate(page); + expect(Number.isNaN(dateFirst)).toBe(false); + // The latest revision is never older than revision 0. + expect(dateLast).toBeGreaterThanOrEqual(dateFirst); + }); +}); diff --git a/src/tests/frontend/specs/timeslider_labels.js b/src/tests/frontend/specs/timeslider_labels.js deleted file mode 100644 index c43552c6b08..00000000000 --- a/src/tests/frontend/specs/timeslider_labels.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -describe('timeslider', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - /** - * @todo test authorsList - */ - it("Shows a date/time in the timeslider and make sure it doesn't include NaN", async function () { - this.timeout(12000); - // make some changes to produce 3 revisions - const revs = 3; - - for (let i = 0; i < revs; i++) { - await helper.edit('a\n'); - } - - await helper.gotoTimeslider(revs); - await helper.waitForPromise(() => helper.contentWindow().location.hash === `#${revs}`); - - // the datetime of last edit - const timerTimeLast = new Date(helper.timesliderTimerTime()).getTime(); - - // the day of this revision, e.g. August 12, 2020 (stripped the string "Saved") - const dateLast = new Date(helper.revisionDateElem().substr(6)).getTime(); - - // the label/revision, ie Version 3 - const labelLast = helper.revisionLabelElem().text(); - - // the datetime should be a date - expect(Number.isNaN(timerTimeLast)).to.eql(false); - - // the Date object of the day should not be NaN - expect(Number.isNaN(dateLast)).to.eql(false); - - // the label should be Version `Number` - expect(labelLast).to.be(`Version ${revs}`); - - // Click somewhere left on the timeslider to go to revision 0 - helper.sliderClick(1); - - // the datetime of last edit - const timerTime = new Date(helper.timesliderTimerTime()).getTime(); - - // the day of this revision, e.g. August 12, 2020 - const date = new Date(helper.revisionDateElem().substr(6)).getTime(); - - // the label/revision, e.g. Version 0 - const label = helper.revisionLabelElem().text(); - - // the datetime should be a date - expect(Number.isNaN(timerTime)).to.eql(false); - // the last revision should be newer or have the same time - expect(timerTimeLast).to.not.be.lessThan(timerTime); - - // the Date object of the day should not be NaN - expect(Number.isNaN(date)).to.eql(false); - - // the label should be Version 0 - expect(label).to.be('Version 0'); - }); -}); diff --git a/src/tests/frontend/specs/timeslider_numeric_padID.js b/src/tests/frontend/specs/timeslider_numeric_padID.js deleted file mode 100644 index a1ba6728f30..00000000000 --- a/src/tests/frontend/specs/timeslider_numeric_padID.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -describe('timeslider', function () { - const padId = 735773577357 + (Math.round(Math.random() * 1000)); - - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad({id: padId}); - }); - - it('Makes sure the export URIs are as expected when the padID is numeric', async function () { - await helper.edit('a\n'); - - await helper.gotoTimeslider(1); - - // ensure we are on revision 1 - await helper.waitForPromise(() => helper.contentWindow().location.hash === '#1'); - - // expect URI to be similar to - // http://192.168.1.48:9001/p/2/1/export/html - // http://192.168.1.48:9001/p/735773577399/1/export/html - const rev1ExportLink = helper.contentWindow().$('#exporthtmla').attr('href'); - expect(rev1ExportLink).to.contain('/1/export/html'); - - // Click somewhere left on the timeslider to go to revision 0 - helper.sliderClick(30); - - const rev0ExportLink = helper.contentWindow().$('#exporthtmla').attr('href'); - expect(rev0ExportLink).to.contain('/0/export/html'); - }); -}); diff --git a/src/tests/frontend/specs/timeslider_revisions.js b/src/tests/frontend/specs/timeslider_revisions.js deleted file mode 100644 index c2de15c42e7..00000000000 --- a/src/tests/frontend/specs/timeslider_revisions.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict'; - -describe('timeslider', function () { - // create a new pad before each test run - beforeEach(async function () { - await helper.aNewPad(); - }); - - it('loads adds a hundred revisions', async function () { - this.timeout(100000); - const chrome$ = helper.padChrome$; - - // Create a bunch of revisions. - for (let i = 0; i < 99; i++) await helper.edit('a'); - chrome$('.buttonicon-savedRevision').trigger('click'); - await helper.waitForPromise(() => helper.padChrome$('.saved-revision').length > 0); - // Give some time to send the SAVE_REVISION message to the server before navigating away. - await new Promise((resolve) => setTimeout(resolve, 100)); - - // go to timeslider - $('#iframe-container iframe') - .attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); - - await new Promise((resolve) => setTimeout(resolve, 6000)); - - const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - const $sliderBar = timeslider$('#ui-slider-bar'); - - const latestContents = timeslider$('#innerdocbody').text(); - - // Click somewhere on the timeslider - let e = new jQuery.Event('mousedown'); - // sets y co-ordinate of the pad slider modal. - const base = (timeslider$('#ui-slider-bar').offset().top - 24); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = base + 5; - $sliderBar.trigger(e); - - e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = base; - $sliderBar.trigger(e); - - e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = base - 5; - $sliderBar.trigger(e); - - $sliderBar.trigger('mouseup'); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // make sure the text has changed - expect(timeslider$('#innerdocbody').text()).not.to.eql(latestContents); - const starIsVisible = timeslider$('.star').is(':visible'); - expect(starIsVisible).to.eql(true); - }); - - - // Disabled as jquery trigger no longer works properly - xit('changes the url when clicking on the timeslider', async function () { - // Create some revisions. - for (let i = 0; i < 20; i++) await helper.edit('a'); - - // go to timeslider - $('#iframe-container iframe') - .attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); - - await new Promise((resolve) => setTimeout(resolve, 6000)); - - const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - const $sliderBar = timeslider$('#ui-slider-bar'); - - const oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash; - - // Click somewhere on the timeslider - const e = new jQuery.Event('mousedown'); - e.clientX = e.pageX = 150; - e.clientY = e.pageY = 60; - $sliderBar.trigger(e); - - await helper.waitForPromise( - () => $('#iframe-container iframe')[0].contentWindow.location.hash !== oldUrl, 6000); - }); - - it('jumps to a revision given in the url', async function () { - const inner$ = helper.padInner$; - this.timeout(40000); - - // wait for the text to be loaded - await helper.waitForPromise(() => inner$('body').text().length !== 0, 10000); - - const newLines = inner$('body div').length; - const oldLength = inner$('body').text().length + newLines / 2; - expect(oldLength).to.not.eql(0); - inner$('div').first().sendkeys('a'); - let timeslider$; - - // wait for our additional revision to be added - await helper.waitForPromise(() => { - // newLines takes the new lines into account which are strippen when using - // inner$('body').text(), one
is used for one line in ACE. - const lenOkay = inner$('body').text().length + newLines / 2 !== oldLength; - // this waits for the color to be added to our , which means that the revision - // was accepted by the server. - const colorOkay = inner$('span').first().attr('class').indexOf('author-') === 0; - return lenOkay && colorOkay; - }, 10000); - - // go to timeslider with a specific revision set - $('#iframe-container iframe') - .attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); - - // wait for the timeslider to be loaded - await helper.waitForPromise(() => { - try { - timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - } catch (e) { - // Empty catch block <3 - } - return timeslider$ && timeslider$('#innerdocbody').text().length === oldLength; - }, 10000); - }); - - it('checks the export url', async function () { - const inner$ = helper.padInner$; - this.timeout(11000); - inner$('div').first().sendkeys('a'); - - await new Promise((resolve) => setTimeout(resolve, 2500)); - - // go to timeslider - $('#iframe-container iframe') - .attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); - let timeslider$; - let exportLink; - - await helper.waitForPromise(() => { - try { - timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - } catch (e) { - // Empty catch block <3 - } - if (!timeslider$) return false; - exportLink = timeslider$('#exportplaina').attr('href'); - if (!exportLink) return false; - return exportLink.substr(exportLink.length - 12) === '0/export/txt'; - }, 6000); - }); -}); From 3f9fe8a1fbe8136642a65465c1a98cdcf023d549 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 12 Jun 2026 11:54:33 +0100 Subject: [PATCH 2/3] test(timeslider): make deep-link spec robust on Firefox Assert the canonical #rev/0 URL and the slider landing on revision 0 rather than the #history-banner-rev label text. The banner label is populated via a MutationObserver bridge that races the iframe load on the bootstrap path in Firefox (it is already covered for the normal button-entry flow by timeslider_revision_labels.spec.ts); the URL + slider value are the deterministic signals that the deep link entered history at the right revision. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/tests/frontend-new/specs/timeslider_deeplink.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tests/frontend-new/specs/timeslider_deeplink.spec.ts b/src/tests/frontend-new/specs/timeslider_deeplink.spec.ts index 143cadebb58..d4441ef9d11 100644 --- a/src/tests/frontend-new/specs/timeslider_deeplink.spec.ts +++ b/src/tests/frontend-new/specs/timeslider_deeplink.spec.ts @@ -16,7 +16,10 @@ test.describe('timeslider deep link', function () { const expectHistoryAtRev0 = async (page: Page) => { await expect(page.locator('body.history-mode')).toBeVisible({timeout: 15000}); await expect(page.locator('#history-controls')).toBeVisible(); - await expect(page.locator('#history-banner-rev')).toHaveText('Version 0', {timeout: 15000}); + // bootstrapFromHash canonicalizes any accepted hash form to #rev/0. + await expect.poll(() => new URL(page.url()).hash, {timeout: 15000}).toBe('#rev/0'); + // The slider lands on revision 0 once pad_mode syncs from the embedded + // BroadcastSlider — the signal that we're actually viewing that revision. await expect.poll( async () => await page.locator('#history-slider-input').evaluate( (el) => Number((el as HTMLInputElement).value)), From 24f4d6de6aa873a0b303deff375e2b84ad107976 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 12 Jun 2026 12:01:58 +0100 Subject: [PATCH 3/3] test(timeslider): address Qodo review on #7949 - Pin locale to en-US in the revision-labels spec so the localized 'Version N' label and 'Saved , ' date assertions are deterministic and the date stays Date-parseable, instead of depending on the runner's locale. - Use a high-entropy numeric pad id (timestamp + random) in the export-links spec so reruns against a persistent DB can't collide on the same pad. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../frontend-new/specs/timeslider_export_links.spec.ts | 6 ++++-- .../frontend-new/specs/timeslider_revision_labels.spec.ts | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tests/frontend-new/specs/timeslider_export_links.spec.ts b/src/tests/frontend-new/specs/timeslider_export_links.spec.ts index 4a910aa2dac..e9ef7adbac7 100644 --- a/src/tests/frontend-new/specs/timeslider_export_links.spec.ts +++ b/src/tests/frontend-new/specs/timeslider_export_links.spec.ts @@ -50,8 +50,10 @@ test.describe('timeslider export links', function () { test('export hrefs target the viewed revision, including a numeric pad id', async function ({page}) { // A numeric pad id is the specific case the legacy test guarded — the - // href rewriter must not confuse it with the revision segment. - const padId = String(735773577357 + Math.floor(Math.random() * 1000)); + // href rewriter must not confuse it with the revision segment. Use a + // high-entropy numeric id (timestamp + random) so reruns against a + // persistent DB can't collide on the same pad. + const padId = `${Date.now()}${Math.floor(Math.random() * 1000)}`; await suppressDeletionTokenModal(page); await goToPad(page, padId); // navigates and waits for the editor to be ready await clearPadContent(page); diff --git a/src/tests/frontend-new/specs/timeslider_revision_labels.spec.ts b/src/tests/frontend-new/specs/timeslider_revision_labels.spec.ts index 84bb7884133..4cfcb70a482 100644 --- a/src/tests/frontend-new/specs/timeslider_revision_labels.spec.ts +++ b/src/tests/frontend-new/specs/timeslider_revision_labels.spec.ts @@ -8,6 +8,10 @@ import {clearPadContent, goToNewPad, writeToPad} from "../helper/padHelper"; // This guards the banner bridge against silently breaking again (cf. #7946). test.describe('timeslider revision labels', function () { test.describe.configure({mode: 'serial'}); + // The "Version N" label and "Saved , " date are localized + // (timeslider.version / timeslider.saved). Pin the locale so the assertions + // are deterministic and the date string stays Date-parseable. + test.use({locale: 'en-US'}); test.beforeEach(async ({context}) => { await context.clearCookies();