discourse/plugins/discourse-local-dates/test/javascripts/integration/rich-editor-extension-test.js
Régis Hanol b2ced65016
FIX: Preserve all date attributes in rich text editor (#36781)
When editing posts in the rich text editor, custom date formats and
advanced options (recurring, timezones, countdown, displayedTimezone)
were silently dropped. Users who carefully configured dates via the
Advanced Mode dialog would lose those settings as soon as they touched
the post in rich text mode.

This happened because the rich-editor-extension only tracked the basic
date/time/timezone attributes, ignoring everything else. Similarly,
copy-pasting date ranges would crash with "No value supplied for
attribute fromDate" because the parseDOM logic was reading attributes
from the wrapper span instead of the inner date spans.

Changes:

- Extend `formatLocalDate` to accept all LocalDateBuilder options so
dates render correctly with custom formats in the editor
- Update rich-editor-extension to parse, store, and serialize all
date attributes for both single dates and date ranges
- Fix date range copy/paste by querying the inner spans via
`dom.querySelector('[data-range="from"]')` instead of reading from
the wrapper element
- Add shared `serializeBBCodeAttr` and `buildBBCodeAttrs` helpers to
`discourse/lib/text.js` that only quote values when necessary
(whitespace or `]` characters)
- Refactor local-dates, poll, calendar, and chat plugins to use the
shared BBCode serialization helpers, eliminating duplicated logic

Internal ref - t/165719

**MARKDOWN (reference)**

<img width="1477" height="548" alt="2025-12-18 @ 18 20 25"
src="https://github.com/user-attachments/assets/f7a20835-ec09-4b41-aff7-6894451fae72"
/>

**RTE (before)**

<img width="759" height="549" alt="2025-12-18 @ 18 22 36"
src="https://github.com/user-attachments/assets/ea914a84-d087-43b6-9007-5807523a6c7c"
/>

**RTE (after)**

<img width="762" height="547" alt="2025-12-18 @ 18 20 35"
src="https://github.com/user-attachments/assets/79c6c70f-80b2-44ff-a1a5-f2564c53b3da"
/>
2025-12-24 14:01:52 +01:00

288 lines
8.2 KiB
JavaScript
Vendored

import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper";
module(
"Integration | Component | prosemirror-editor - local-dates plugin extension",
function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.siteSettings.rich_editor = true;
});
function findDate() {
return document.querySelector(".ProseMirror .discourse-local-date");
}
function findRange() {
return document.querySelector(".ProseMirror .discourse-local-date-range");
}
test("local date", async function (assert) {
const markdown = "[date=2021-01-01 time=12:00:00]";
await testMarkdown(
assert,
markdown,
() => {
const span = findDate();
assert.dom(span).exists();
assert.strictEqual(span.dataset.date, "2021-01-01");
assert.strictEqual(span.dataset.time, "12:00:00");
},
markdown
);
});
test("local date without time", async function (assert) {
const markdown = "[date=2021-01-01]";
await testMarkdown(
assert,
markdown,
() => {
const span = findDate();
assert.dom(span).exists();
assert.strictEqual(span.dataset.date, "2021-01-01");
assert.strictEqual(span.dataset.time, undefined);
},
markdown
);
});
test("local date with timezone", async function (assert) {
const markdown =
"[date=2021-01-01 time=12:00:00 timezone=America/New_York]";
await testMarkdown(
assert,
markdown,
() => {
assert.strictEqual(findDate().dataset.timezone, "America/New_York");
},
markdown
);
});
test("local date with format", async function (assert) {
const markdown =
'[date=2021-01-01 time=12:00:00 format="YYYY-MM-DD HH:mm"]';
await testMarkdown(
assert,
markdown,
() => {
const span = findDate();
assert.strictEqual(span.dataset.format, "YYYY-MM-DD HH:mm");
assert.true(span.textContent.includes("2021-01-01"));
},
markdown
);
});
test("local date with recurring", async function (assert) {
const markdown = "[date=2021-01-01 time=12:00:00 recurring=1.weeks]";
await testMarkdown(
assert,
markdown,
() => {
assert.strictEqual(findDate().dataset.recurring, "1.weeks");
},
markdown
);
});
test("local date with timezones", async function (assert) {
const markdown =
"[date=2021-01-01 time=12:00:00 timezones=Europe/Paris|Asia/Tokyo]";
await testMarkdown(
assert,
markdown,
() => {
assert.strictEqual(
findDate().dataset.timezones,
"Europe/Paris|Asia/Tokyo"
);
},
markdown
);
});
test("local date with countdown", async function (assert) {
const markdown = "[date=2099-01-01 time=12:00:00 countdown=true]";
await testMarkdown(
assert,
markdown,
() => {
assert.strictEqual(findDate().dataset.countdown, "true");
},
markdown
);
});
test("local date with displayedTimezone", async function (assert) {
const markdown =
"[date=2021-01-01 time=12:00:00 displayedTimezone=Europe/London]";
await testMarkdown(
assert,
markdown,
() => {
assert.strictEqual(
findDate().dataset.displayedTimezone,
"Europe/London"
);
},
markdown
);
});
test("local date range", async function (assert) {
const markdown = "[date-range from=2021-01-01 to=2021-01-02]";
await testMarkdown(
assert,
markdown,
() => {
const rangeSpan = findRange();
assert.dom(rangeSpan).exists();
const fromSpan = rangeSpan.querySelector('[data-range="from"]');
const toSpan = rangeSpan.querySelector('[data-range="to"]');
assert.strictEqual(fromSpan.dataset.date, "2021-01-01");
assert.strictEqual(toSpan.dataset.date, "2021-01-02");
},
markdown
);
});
test("local date range with time", async function (assert) {
const markdown =
"[date-range from=2021-01-01T12:00:00 to=2021-01-02T13:00:00]";
await testMarkdown(
assert,
markdown,
() => {
const rangeSpan = findRange();
const fromSpan = rangeSpan.querySelector('[data-range="from"]');
const toSpan = rangeSpan.querySelector('[data-range="to"]');
assert.strictEqual(fromSpan.dataset.date, "2021-01-01");
assert.strictEqual(fromSpan.dataset.time, "12:00:00");
assert.strictEqual(toSpan.dataset.date, "2021-01-02");
assert.strictEqual(toSpan.dataset.time, "13:00:00");
},
markdown
);
});
test("local date range with timezone", async function (assert) {
const markdown =
"[date-range from=2021-01-01 to=2021-01-02 timezone=America/New_York]";
await testMarkdown(
assert,
markdown,
() => {
const rangeSpan = findRange();
const fromSpan = rangeSpan.querySelector('[data-range="from"]');
const toSpan = rangeSpan.querySelector('[data-range="to"]');
assert.strictEqual(fromSpan.dataset.timezone, "America/New_York");
assert.strictEqual(toSpan.dataset.timezone, "America/New_York");
},
markdown
);
});
test("local date range with format", async function (assert) {
const markdown =
"[date-range from=2021-01-01 to=2021-01-02 format=YYYY-MM-DD]";
await testMarkdown(
assert,
markdown,
() => {
const rangeSpan = findRange();
const fromSpan = rangeSpan.querySelector('[data-range="from"]');
const toSpan = rangeSpan.querySelector('[data-range="to"]');
assert.strictEqual(fromSpan.dataset.format, "YYYY-MM-DD");
assert.strictEqual(toSpan.dataset.format, "YYYY-MM-DD");
},
markdown
);
});
test("local date range with timezones", async function (assert) {
const markdown =
"[date-range from=2021-01-01 to=2021-01-02 timezones=Europe/Paris|Asia/Tokyo]";
await testMarkdown(
assert,
markdown,
() => {
const rangeSpan = findRange();
const fromSpan = rangeSpan.querySelector('[data-range="from"]');
const toSpan = rangeSpan.querySelector('[data-range="to"]');
assert.strictEqual(
fromSpan.dataset.timezones,
"Europe/Paris|Asia/Tokyo"
);
assert.strictEqual(
toSpan.dataset.timezones,
"Europe/Paris|Asia/Tokyo"
);
},
markdown
);
});
test("local date range with countdown", async function (assert) {
const markdown =
"[date-range from=2099-01-01 to=2099-01-02 countdown=true]";
await testMarkdown(
assert,
markdown,
() => {
const rangeSpan = findRange();
const fromSpan = rangeSpan.querySelector('[data-range="from"]');
const toSpan = rangeSpan.querySelector('[data-range="to"]');
assert.strictEqual(fromSpan.dataset.countdown, "true");
assert.strictEqual(toSpan.dataset.countdown, "true");
},
markdown
);
});
test("local date range with displayedTimezone", async function (assert) {
const markdown =
"[date-range from=2021-01-01 to=2021-01-02 displayedTimezone=Europe/London]";
await testMarkdown(
assert,
markdown,
() => {
const rangeSpan = findRange();
const fromSpan = rangeSpan.querySelector('[data-range="from"]');
const toSpan = rangeSpan.querySelector('[data-range="to"]');
assert.strictEqual(
fromSpan.dataset.displayedTimezone,
"Europe/London"
);
assert.strictEqual(toSpan.dataset.displayedTimezone, "Europe/London");
},
markdown
);
});
}
);