ep_comments_page 11.0.28 → 11.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/locales/en.json CHANGED
@@ -14,6 +14,8 @@
14
14
  "ep_comments_page.comments_template.suggested_change_from" : "Suggested change from \"{{changeFrom}}\" to \"{{changeTo}}\"",
15
15
  "ep_comments_page.comments_template.suggested_change_from_label" : "Suggested change from",
16
16
  "ep_comments_page.comments_template.suggest_change_from" : "Suggest change from \"{{changeFrom}}\" to",
17
+ "ep_comments_page.comments_template.suggest_change_from_label" : "Suggest change from",
18
+ "ep_comments_page.comments_template.suggest_change_to_label" : "to",
17
19
  "ep_comments_page.comments_template.to" : "To",
18
20
  "ep_comments_page.comments_template.include_suggestion" : "Include suggested change",
19
21
  "ep_comments_page.comments_template.comment.value" : "Comment",
package/locales/ko.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "@metadata": {
3
3
  "authors": [
4
4
  "Dr1t jg",
5
+ "Tensama0415",
5
6
  "Ykhwong",
6
7
  "그냥기여자",
7
8
  "아라"
@@ -20,6 +21,7 @@
20
21
  "ep_comments_page.comments_template.accept_change.value": "변경 수락",
21
22
  "ep_comments_page.comments_template.revert_change.value": "변경사항 되돌리기",
22
23
  "ep_comments_page.comments_template.suggested_change_from": "\"{{changeFrom}}\"에서 \"{{changeTo}}\"(으)로 변경 제안됨",
24
+ "ep_comments_page.comments_template.suggested_change_from_label": "다음으로부터 변경 제안됨:",
23
25
  "ep_comments_page.comments_template.suggest_change_from": "\"{{changeFrom}}\"에서 다음으로 변경할 것으로 제안:",
24
26
  "ep_comments_page.comments_template.to": "도착지",
25
27
  "ep_comments_page.comments_template.include_suggestion": "제안된 변경사항 포함",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "description": "Adds comments on sidebar and link it to the text. For no-skin use ep_page_view.",
3
3
  "name": "ep_comments_page",
4
- "version": "11.0.28",
4
+ "version": "11.0.29",
5
5
  "author": {
6
6
  "name": "Nicolas Lescop",
7
7
  "email": "limplementeur@gmail.com"
@@ -56,6 +56,10 @@ input.error, textarea.error {
56
56
  .comment-actions-wrapper {
57
57
  float: right;
58
58
  }
59
+ .comment-actions-wrapper .comment-edit,
60
+ .comment-actions-wrapper .comment-delete {
61
+ display: inline;
62
+ }
59
63
 
60
64
  /* COMMENT COMPACTED (Visible on right side) */
61
65
  .sidebar-comment:not(.full-display) .full-display-content {
@@ -127,6 +131,16 @@ input.error, textarea.error {
127
131
  font-weight: bold;
128
132
  margin: 5px 0;
129
133
  }
134
+ .suggestion-create .from-value {
135
+ display: block;
136
+ opacity: .8;
137
+ font-style: italic;
138
+ margin: 5px 0;
139
+ }
140
+ .suggestion-create .from-value:before,
141
+ .suggestion-create .from-value:after {
142
+ content: '"';
143
+ }
130
144
  .approve-suggestion-btn, .revert-suggestion-btn {
131
145
  display: block;
132
146
  margin-bottom: 10px;
@@ -12,6 +12,11 @@ const commentIcons = require('ep_comments_page/static/js/commentIcons');
12
12
  const commentL10n = require('ep_comments_page/static/js/commentL10n');
13
13
  const events = require('ep_comments_page/static/js/copyPasteEvents');
14
14
  const moment = require('ep_comments_page/static/js/moment-with-locales.min');
15
+ // Expose the moment instance on the chrome window so frontend tests (which
16
+ // no longer have `window.require` available in the Playwright bundle) can
17
+ // share locale state with the plugin. Harmless in production — a single
18
+ // read-only reference to the already-loaded module.
19
+ if (typeof window !== 'undefined') window.__epcpMoment = moment;
15
20
  const newComment = require('ep_comments_page/static/js/newComment');
16
21
  const padcookie = require('ep_etherpad-lite/static/js/pad_cookie').padcookie;
17
22
  const preCommentMark = require('ep_comments_page/static/js/preCommentMark');
@@ -56,5 +56,43 @@ describe(__filename, function () {
56
56
  // The template references the new label key.
57
57
  assert(template.includes(`data-l10n-id="${labelKey}"`),
58
58
  `comments.html should reference ${labelKey} for the suggestion display label`);
59
+
60
+ // The display-suggestion block must surface the actual changeFrom /
61
+ // changeTo values to the user. Hiding both spans (as the original
62
+ // #273 fix accidentally left them) leaves "Suggested change from"
63
+ // followed by nothing — the user can no longer see what the
64
+ // suggestion is.
65
+ const display = template.match(/<script id="display-suggestion"[\s\S]*?<\/script>/);
66
+ assert(display, 'display-suggestion template must exist');
67
+ assert(!/class="hidden from-value"/.test(display[0]),
68
+ '.from-value in display-suggestion must be visible');
69
+ assert(!/class="hidden to-value"/.test(display[0]),
70
+ '.to-value in display-suggestion must be visible');
71
+ });
72
+
73
+ it('new-comment form label does not use a key with unresolved placeholders',
74
+ function () {
75
+ // The new-comment template previously used `suggest_change_from`
76
+ // whose English value contains "{{changeFrom}}". The data-l10n-args
77
+ // attribute relied on jquery.tmpl substituting the selected text into
78
+ // valid JSON, which broke whenever the selection contained quotes or
79
+ // backslashes. The replacement label key must not require args.
80
+ const fromLabel = 'ep_comments_page.comments_template.suggest_change_from_label';
81
+ const toLabel = 'ep_comments_page.comments_template.suggest_change_to_label';
82
+ for (const key of [fromLabel, toLabel]) {
83
+ assert(Object.prototype.hasOwnProperty.call(en, key),
84
+ `expected ${key} in en.json`);
85
+ assert(!/\{\{/.test(en[key]),
86
+ `${key} must not contain {{placeholders}}; got: ${en[key]}`);
87
+ }
88
+ assert(template.includes(`data-l10n-id="${fromLabel}"`),
89
+ `comments.html should reference ${fromLabel} for the new-comment label`);
90
+ assert(template.includes(`data-l10n-id="${toLabel}"`),
91
+ `comments.html should reference ${toLabel} for the new-comment label`);
92
+
93
+ // The placeholder-laden key must not be referenced from the template.
94
+ const placeholderKey = 'ep_comments_page.comments_template.suggest_change_from';
95
+ assert(!template.includes(`data-l10n-id="${placeholderKey}"`),
96
+ 'comments.html must not reference the placeholder-laden suggest_change_from key');
59
97
  });
60
98
  });
@@ -16,6 +16,23 @@ export const reopenCommentsPad = async (page: Page, padId: string): Promise<void
16
16
  await waitForCommentsInit(page);
17
17
  };
18
18
 
19
+ // Reopen the same pad as a fresh user so a new authorId is allocated. Used
20
+ // by the delete/edit "other user" specs which assert that one user cannot
21
+ // modify another user's comment. Plain reopenCommentsPad reuses the same
22
+ // session cookies (token / express_sid), so Etherpad keeps the original
23
+ // authorId and the auth-check on delete/edit short-circuits to success.
24
+ export const reopenCommentsPadAsFreshUser = async (
25
+ page: Page, padId: string,
26
+ ): Promise<void> => {
27
+ await page.context().clearCookies();
28
+ await page.evaluate(() => {
29
+ try { window.localStorage.clear(); } catch {}
30
+ try { window.sessionStorage.clear(); } catch {}
31
+ }).catch(() => {});
32
+ await goToPad(page, padId);
33
+ await waitForCommentsInit(page);
34
+ };
35
+
19
36
  export const waitForCommentsInit = async (page: Page): Promise<void> => {
20
37
  await expect.poll(async () => page.evaluate(async () => {
21
38
  const w = window as any;
@@ -86,7 +103,10 @@ export const fillCommentForm = async (
86
103
  const field = page.locator('textarea.comment-content');
87
104
  await field.fill(commentText);
88
105
  if (suggestion !== undefined) {
89
- await page.locator('#newComment .suggestion-checkbox').first().click();
106
+ // Click the label (not the input): the adjacent <label> intercepts pointer
107
+ // events on the small native checkbox in some browsers, so click the label
108
+ // — which is also how a user toggles the checkbox in the UI.
109
+ await page.locator('#newComment .label-suggestion-checkbox').first().click();
90
110
  await page.locator('textarea.to-value').fill(suggestion);
91
111
  }
92
112
  };
@@ -153,20 +173,46 @@ export const addReplyToLine = async (
153
173
 
154
174
  if (await commentIconsEnabled(page)) {
155
175
  await o.locator(`#commentIcons #icon-${commentId}`).first().click();
176
+ } else {
177
+ // Reply form is inside .full-display-content which is display:none on
178
+ // the sidebar comment until it gets the .full-display class. Hover the
179
+ // sidebar comment to trigger commentBoxes.highlightComment, which sets
180
+ // it; otherwise locator.fill() times out on a hidden input.
181
+ await o.locator(`#${commentId}`).first().hover();
182
+ await expect.poll(async () =>
183
+ o.locator(`#${commentId}.full-display`).count()).toBeGreaterThan(0);
156
184
  }
157
185
 
158
- await o.locator('.comment-content').first().fill(replyText);
186
+ // Scope to the reply form's input — the new-comment popup may also
187
+ // have a `.comment-content` element open at this point.
188
+ const replyForm = o.locator(`#${commentId} form.new-comment`).first();
189
+ await replyForm.locator('.comment-content').fill(replyText);
159
190
  if (withSuggestion) {
160
- await o.locator('.suggestion-checkbox').first().click();
191
+ await replyForm.locator('.label-suggestion-checkbox').first().click();
161
192
  if (suggestionText !== undefined) {
162
- await o.locator('textarea.to-value').first().fill(suggestionText);
193
+ await replyForm.locator('textarea.to-value').first().fill(suggestionText);
163
194
  }
164
195
  }
165
- await o.locator("form.new-comment input[type='submit']").first().click();
196
+ await replyForm.locator("input[type='submit']").first().click();
166
197
  await expect.poll(async () => o.locator('.sidebar-comment-reply').count())
167
198
  .toBe(existing + 1);
168
199
  };
169
200
 
201
+ // Expand a sidebar comment so its .full-display-content (containing
202
+ // .comment-edit / .comment-delete / reply form) is visible. The plugin
203
+ // only adds .full-display on mouseover, so any test that touches those
204
+ // children after a fresh page load needs to call this first.
205
+ export const expandSidebarComment = async (
206
+ page: Page,
207
+ commentId: string,
208
+ ): Promise<void> => {
209
+ const o = await getPadOuter(page);
210
+ await expect.poll(async () => o.locator(`#${commentId}`).count()).toBeGreaterThan(0);
211
+ await o.locator(`#${commentId}`).first().hover();
212
+ await expect.poll(async () =>
213
+ o.locator(`#${commentId}.full-display`).count()).toBeGreaterThan(0);
214
+ };
215
+
170
216
  // Open Etherpad settings, toggle Show Comments to desired state, close settings.
171
217
  export const chooseToShowComments = async (
172
218
  page: Page, shouldShow: boolean,
@@ -0,0 +1,71 @@
1
+ import {expect, test} from '@playwright/test';
2
+ import {getPadBody, getPadOuter}
3
+ from 'ep_etherpad-lite/tests/frontend-new/helper/padHelper';
4
+ import {
5
+ addCommentToLine,
6
+ addReplyToLine,
7
+ aNewCommentsPad,
8
+ enlargeScreen,
9
+ expandSidebarComment,
10
+ reopenCommentsPadAsFreshUser,
11
+ setPadLines,
12
+ waitForCommentOnLine,
13
+ } from '../helper/comments';
14
+
15
+ const textOfComment = 'original comment';
16
+ const textOfReply = 'original reply';
17
+ const FIRST_LINE = 0;
18
+
19
+ test.describe('ep_comments_page - Comment Delete', () => {
20
+ let padId: string;
21
+
22
+ test.beforeEach(async ({page}) => {
23
+ test.setTimeout(60_000);
24
+ padId = await aNewCommentsPad(page);
25
+ await enlargeScreen(page);
26
+ await setPadLines(page, ['something', ' anything']);
27
+ await addCommentToLine(page, FIRST_LINE, textOfComment);
28
+ await addReplyToLine(page, FIRST_LINE, textOfReply);
29
+ });
30
+
31
+ test.describe('when user presses the delete button on a comment', () => {
32
+ test('should delete comment', async ({page}) => {
33
+ const outer = await getPadOuter(page);
34
+ const inner = await getPadBody(page);
35
+ await outer.locator('.comment-delete').first().click();
36
+ await expect.poll(async () => inner.locator('.comment').count()).toBe(0);
37
+ });
38
+ });
39
+
40
+ test.describe('when viewing another user\'s comment', () => {
41
+ test('the delete action is hidden so the comment cannot be removed',
42
+ async ({page}) => {
43
+ // The plugin enforces "only the author can delete" at the UI
44
+ // layer: insertComment() in static/js/index.js adds a `hidden`
45
+ // class to .comment-actions-wrapper whenever
46
+ // comment.author !== clientVars.userId. Re-open with cleared
47
+ // cookies / storage so Etherpad allocates a new authorId, then
48
+ // assert the delete button is unreachable.
49
+ await page.waitForTimeout(500);
50
+ await reopenCommentsPadAsFreshUser(page, padId);
51
+ await enlargeScreen(page);
52
+
53
+ const outer = await getPadOuter(page);
54
+ const inner = await getPadBody(page);
55
+ const commentId = await waitForCommentOnLine(page, FIRST_LINE);
56
+ await expandSidebarComment(page, commentId);
57
+
58
+ // The actions wrapper for this comment carries the `hidden`
59
+ // class, so its child .comment-delete is not visible.
60
+ const wrapper = outer.locator(
61
+ `#${commentId} .comment-actions-wrapper`).first();
62
+ await expect.poll(async () => wrapper.evaluate(
63
+ (el) => el.classList.contains('hidden'))).toBe(true);
64
+ expect(await outer.locator(`#${commentId} .comment-delete`).first()
65
+ .isVisible()).toBe(false);
66
+
67
+ // Comment is still intact in the editor.
68
+ expect(await inner.locator('.comment').count()).toBeGreaterThan(0);
69
+ });
70
+ });
71
+ });
@@ -6,8 +6,11 @@ import {
6
6
  addReplyToLine,
7
7
  aNewCommentsPad,
8
8
  enlargeScreen,
9
+ expandSidebarComment,
9
10
  reopenCommentsPad,
11
+ reopenCommentsPadAsFreshUser,
10
12
  setPadLines,
13
+ waitForCommentOnLine,
11
14
  } from '../helper/comments';
12
15
 
13
16
  const textOfComment = 'original comment';
@@ -88,26 +91,32 @@ test.describe('ep_comments_page - Comment Edit', () => {
88
91
  {timeout: 20_000}).toBe(updatedText);
89
92
  });
90
93
 
91
- test('new user tries editing should not update the comment text', async ({page}) => {
92
- const updatedText2 = 'this comment was edited again';
93
- await page.waitForTimeout(500);
94
- // Reopen as a "new" user (fresh page load).
95
- await reopenCommentsPad(page, padId);
96
- const outer = await getPadOuter(page);
97
- await expect.poll(async () =>
98
- outer.locator('#comments .comment-edit').count()).toBeGreaterThan(0);
94
+ test('a new user cannot reach the edit action for another user\'s comment',
95
+ async ({page}) => {
96
+ await page.waitForTimeout(500);
97
+ // Reopen with cleared cookies / storage so Etherpad allocates a
98
+ // new authorId.
99
+ await reopenCommentsPadAsFreshUser(page, padId);
100
+ await enlargeScreen(page);
101
+ const outer = await getPadOuter(page);
102
+ const inner = await getPadBody(page);
103
+ const commentId = await waitForCommentOnLine(page, FIRST_LINE);
104
+ await expandSidebarComment(page, commentId);
99
105
 
100
- await outer.locator('.comment-edit').first().click();
101
- await outer.locator('.comment-edit-form .comment-edit-text').first()
102
- .evaluate((el, t) => { (el as HTMLElement).innerText = t; }, updatedText2);
103
- await outer.locator('.comment-edit-form .comment-edit-submit').first().click();
106
+ // The plugin hides the entire .comment-actions-wrapper for
107
+ // non-authors at insertComment() time (see static/js/index.js),
108
+ // so the edit button is unreachable from the UI. Assert the
109
+ // wrapper carries `hidden` and the edit glyph is invisible.
110
+ const wrapper = outer.locator(
111
+ `#${commentId} .comment-actions-wrapper`).first();
112
+ await expect.poll(async () => wrapper.evaluate(
113
+ (el) => el.classList.contains('hidden'))).toBe(true);
114
+ expect(await outer.locator(`#${commentId} .comment-edit`).first()
115
+ .isVisible()).toBe(false);
104
116
 
105
- // Error gritter is shown.
106
- await expect.poll(async () =>
107
- page.locator('#gritter-container .error').count()).toBeGreaterThan(0);
108
- // Comment text is not the new text.
109
- const got = (await outer.locator('.comment-text').first().textContent())?.trim();
110
- expect(got).not.toBe(updatedText2);
111
- });
117
+ // Comment text in the inner pad is still the original.
118
+ expect((await inner.locator('.comment').first().textContent())?.trim())
119
+ .not.toBe('');
120
+ });
112
121
  });
113
122
  });
@@ -20,7 +20,7 @@ const createReply = async (page: import('@playwright/test').Page, withSuggestion
20
20
  }
21
21
  await outer.locator('.comment-content').first().fill('My reply');
22
22
  if (withSuggestion) {
23
- await outer.locator('.suggestion-checkbox').first().click();
23
+ await outer.locator('.label-suggestion-checkbox').first().click();
24
24
  await outer.locator('textarea.to-value').first().fill('My suggestion');
25
25
  }
26
26
  await outer.locator("form.new-comment input[type='submit']").first().click();
@@ -40,7 +40,7 @@ test.describe('ep_comments_page - Comment Reply', () => {
40
40
  await page.locator('.addComment').first().click();
41
41
  await page.locator('textarea.comment-content').fill('My comment');
42
42
  const outer = await getPadOuter(page);
43
- await outer.locator('.suggestion-checkbox').first().click();
43
+ await outer.locator('.label-suggestion-checkbox').first().click();
44
44
  await outer.locator('textarea.to-value').first().fill('Change to this suggestion');
45
45
  await page.locator('.comment-buttons input[type=submit]').first().click();
46
46
  await waitForCommentOnLine(page, 0);
@@ -29,7 +29,7 @@ const openCommentFormWithSuggestion = async (
29
29
  await page.locator('.addComment').first().click();
30
30
  await expect.poll(async () =>
31
31
  page.locator('#newComment.popup-show .suggestion-checkbox').count()).toBeGreaterThan(0);
32
- await page.locator('#newComment.popup-show .suggestion-checkbox').first().click();
32
+ await page.locator('#newComment.popup-show .label-suggestion-checkbox').first().click();
33
33
  };
34
34
 
35
35
  test.describe('ep_comments_page - Comment Suggestion', () => {
@@ -83,11 +83,15 @@ test.describe('ep_comments_page - Comment Suggestion', () => {
83
83
  await expect.poll(async () =>
84
84
  outer.locator('.comment-container .full-display-content:visible').count())
85
85
  .toBeGreaterThan(0);
86
+ // The suggested ("to") text now lives in its own .to-value span, not
87
+ // inside the localized label — the placeholder-laden key was retired
88
+ // (see #379). Assert the value round-trips into that span (special
89
+ // chars and all) instead of substring-matching the label.
86
90
  await expect.poll(async () => {
87
- const t = await outer.locator('.comment-container .comment-title-wrapper .from-label')
91
+ const t = await outer.locator('.comment-container .comment-title-wrapper .to-value')
88
92
  .first().textContent();
89
- return t && t.includes(suggestedText);
90
- }).toBe(true);
93
+ return (t || '').trim();
94
+ }).toBe(suggestedText);
91
95
 
92
96
  await outer.locator('.approve-suggestion-btn:visible').first().click();
93
97
  await expect.poll(async () =>
@@ -21,7 +21,7 @@ test.describe('ep_comments_page - l10n', () => {
21
21
  await inner.locator('div').first().click({clickCount: 3});
22
22
  await page.locator('.addComment').first().click();
23
23
  await page.locator('textarea.comment-content').fill('My comment');
24
- await page.locator('#newComment .suggestion-checkbox').first().click();
24
+ await page.locator('#newComment .label-suggestion-checkbox').first().click();
25
25
  await page.locator('textarea.to-value').fill(suggestedText);
26
26
  await page.locator('.comment-buttons input[type=submit]').first().click();
27
27
  await waitForCommentOnLine(page, 0);
@@ -45,8 +45,23 @@ test.describe('ep_comments_page - l10n', () => {
45
45
  await changeLanguageTo(page, 'pt-br');
46
46
  const outer = await getPadOuter(page);
47
47
  const commentId = await getCommentIdOfLine(page, 0);
48
- const text = await outer.locator(`#${commentId} .from-label`).first().textContent();
49
- expect(text).toBe(`Alteração sugerida de "${commentedText}" para "${suggestedText}"`);
48
+ // The display-suggestion block now renders four sibling spans
49
+ // (from-label / from-value / to-label / to-value) instead of a single
50
+ // string with {{changeFrom}} / {{changeTo}} placeholders, so the
51
+ // assertion covers the assembled text rather than a single key.
52
+ //
53
+ // Until translatewiki syncs `suggested_change_from_label` and
54
+ // `suggest_change_to_label` for non-English locales, those labels fall
55
+ // back to English even when the page locale is pt-br (tracked in
56
+ // ether/ep_comments_page#379). The values themselves do not depend on
57
+ // locale, so we still assert they round-trip into the rendered DOM.
58
+ const block = outer.locator(`#${commentId} .suggestion-display`).first();
59
+ expect((await block.locator('.from-label').first().textContent() || '').trim())
60
+ .not.toBe('');
61
+ expect((await block.locator('.from-value').first().textContent() || '').trim())
62
+ .toBe(commentedText);
63
+ expect((await block.locator('.to-value').first().textContent() || '').trim())
64
+ .toBe(suggestedText);
50
65
  });
51
66
 
52
67
  test("localizes 'new comment' form when Etherpad language is changed", async ({page}) => {
@@ -35,7 +35,7 @@ test.describe('ep_comments_page - Comment settings', () => {
35
35
  await inner.locator('div').first().click({clickCount: 3});
36
36
  await page.locator('.addComment').first().click();
37
37
  await page.locator('textarea.comment-content').fill('My comment');
38
- await outer.locator('.suggestion-checkbox').first().click();
38
+ await outer.locator('.label-suggestion-checkbox').first().click();
39
39
  await outer.locator('textarea.to-value').first().fill('Change to this suggestion');
40
40
  await page.locator('.comment-buttons input[type=submit]').first().click();
41
41
  // After creating the comment, click Add Comment again — sidebar must remain hidden.
@@ -101,7 +101,7 @@ test.describe('ep_comments_page - Pre-comment text mark', () => {
101
101
  if (!(await highlightSelectedTextEnabled(page))) return;
102
102
  const outer = await getPadOuter(page);
103
103
  await page.locator('textarea.comment-content').fill('My comment');
104
- await outer.locator('.suggestion-checkbox').first().click();
104
+ await outer.locator('.label-suggestion-checkbox').first().click();
105
105
  await outer.locator('textarea.to-value').first().fill('Change to this suggestion');
106
106
  await page.locator('.comment-buttons input[type=submit]').first().click();
107
107
  await waitForCommentOnLine(page, 0);
@@ -10,15 +10,21 @@ const months = (n: number) => 4 * weeks(n);
10
10
  const years = (n: number) => 12 * months(n);
11
11
 
12
12
  // Evaluate `moment(<offsetMs from now>).fromNow()` inside the chrome window.
13
- // We initialise moment lazily on first call and set its 'ss' threshold to 0 to
14
- // match the legacy spec's `moment.relativeTimeThreshold('ss', 0)`.
13
+ // We initialise moment lazily on first call and set its 'ss' threshold to 0
14
+ // to match the legacy spec's `moment.relativeTimeThreshold('ss', 0)`.
15
+ //
16
+ // The plugin attaches its CommonJS-required moment instance to
17
+ // window.__epcpMoment for tests (see static/js/index.js). The legacy spec
18
+ // called `window.require(...)` directly, but the Playwright bundle no
19
+ // longer exposes `require` on the chrome window, so we read the
20
+ // plugin-published handle instead — sharing one instance keeps locale
21
+ // changes from changeLanguageTo() observable in the tests.
15
22
  const initMoment = async (page: import('@playwright/test').Page) => {
23
+ await expect.poll(async () =>
24
+ page.evaluate(() => Boolean((window as any).__epcpMoment)),
25
+ {timeout: 10_000}).toBe(true);
16
26
  await page.evaluate(() => {
17
- const w = window as any;
18
- if (w.__epcpMoment) return;
19
- w.__epcpMoment = w.require(
20
- 'ep_comments_page/static/js/moment-with-locales.min');
21
- w.__epcpMoment.relativeTimeThreshold('ss', 0);
27
+ (window as any).__epcpMoment.relativeTimeThreshold('ss', 0);
22
28
  });
23
29
  };
24
30
 
@@ -27,8 +27,9 @@
27
27
  <label for="suggestion-checkbox-${commentId}" class="label-suggestion-checkbox" data-l10n-id="ep_comments_page.comments_template.include_suggestion">Include suggested change</label>
28
28
  </p>
29
29
  <div class="suggestion suggestion-create">
30
- <span class="from-label" data-l10n-id="ep_comments_page.comments_template.suggest_change_from" data-l10n-args='{"changeFrom": "${changeFrom}"}'>Suggest Change From</span>
31
- <span class="hidden from-value">${changeFrom}</span>
30
+ <span class="from-label" data-l10n-id="ep_comments_page.comments_template.suggest_change_from_label">Suggest change from</span>
31
+ <span class="from-value">${changeFrom}</span>
32
+ <span class="to-label" data-l10n-id="ep_comments_page.comments_template.suggest_change_to_label">to</span>
32
33
  <textarea class="to-value"></textarea>
33
34
  </div>
34
35
 
@@ -98,8 +99,9 @@
98
99
  <form class="comment-changeTo-form suggestion-display">
99
100
  <div>
100
101
  <span class="from-label" data-l10n-id="ep_comments_page.comments_template.suggested_change_from_label">Suggested change from</span>
101
- <span class="hidden from-value">${changeFrom}</span>
102
- <span class="hidden to-value">${changeTo}</span>
102
+ <span class="from-value">${changeFrom}</span>
103
+ <span class="to-label" data-l10n-id="ep_comments_page.comments_template.suggest_change_to_label">to</span>
104
+ <span class="to-value">${changeTo}</span>
103
105
  </div>
104
106
  <!-- Approve/revert button -->
105
107
  <input type="Submit" class="btn btn-primary approve-suggestion-btn acl-write" value="Accept Change" data-l10n-id="ep_comments_page.comments_template.accept_change.value">
@@ -1,57 +0,0 @@
1
- import {expect, test} from '@playwright/test';
2
- import {getPadBody, getPadOuter}
3
- from 'ep_etherpad-lite/tests/frontend-new/helper/padHelper';
4
- import {
5
- addCommentToLine,
6
- addReplyToLine,
7
- aNewCommentsPad,
8
- enlargeScreen,
9
- reopenCommentsPad,
10
- setPadLines,
11
- } from '../helper/comments';
12
-
13
- const textOfComment = 'original comment';
14
- const textOfReply = 'original reply';
15
- const FIRST_LINE = 0;
16
-
17
- test.describe('ep_comments_page - Comment Delete', () => {
18
- let padId: string;
19
-
20
- test.beforeEach(async ({page}) => {
21
- test.setTimeout(60_000);
22
- padId = await aNewCommentsPad(page);
23
- await enlargeScreen(page);
24
- await setPadLines(page, ['something', ' anything']);
25
- await addCommentToLine(page, FIRST_LINE, textOfComment);
26
- await addReplyToLine(page, FIRST_LINE, textOfReply);
27
- });
28
-
29
- test.describe('when user presses the delete button on a comment', () => {
30
- test('should delete comment', async ({page}) => {
31
- const outer = await getPadOuter(page);
32
- const inner = await getPadBody(page);
33
- await outer.locator('.comment-delete').first().click();
34
- await expect.poll(async () => inner.locator('.comment').count()).toBe(0);
35
- });
36
- });
37
-
38
- test.describe('when user presses the delete button on other users comment', () => {
39
- test('should not delete comment', async ({page}) => {
40
- // Reload as a fresh user — re-opens the same pad and waits for plugin init.
41
- await page.waitForTimeout(500);
42
- await reopenCommentsPad(page, padId);
43
-
44
- const outer = await getPadOuter(page);
45
- const inner = await getPadBody(page);
46
- await expect.poll(async () => outer.locator('.comment-delete').count())
47
- .toBeGreaterThan(0);
48
- await outer.locator('.comment-delete').first().click();
49
-
50
- // Error gritter shown.
51
- await expect.poll(async () =>
52
- page.locator('#gritter-container .error').count()).toBeGreaterThan(0);
53
- // Comment was not deleted.
54
- expect(await inner.locator('.comment').count()).toBeGreaterThan(0);
55
- });
56
- });
57
- });
@@ -1,32 +0,0 @@
1
- import {expect, test} from '@playwright/test';
2
- import {getPadBody, goToNewPad} from 'ep_etherpad-lite/tests/frontend-new/helper/padHelper';
3
-
4
- // The faithful 1:1 port of the legacy mocha specs (11 spec files /
5
- // ~100 test() blocks) lives in ../parked/, outside the playwright
6
- // glob. The most recent reliable run had 150 passing / 93 failing /
7
- // 27 skipped — most failures are real per-test bugs that need
8
- // individual investigation, and several lean on legacy patterns
9
- // (chrome$/window globals) that don't carry over cleanly. Keep the
10
- // port for modernization but don't block the release pipeline on it.
11
-
12
- test.beforeEach(async ({page}) => {
13
- await goToNewPad(page);
14
- });
15
-
16
- test.describe('ep_comments_page', () => {
17
- test('pad loads with plugin installed', async ({page}) => {
18
- const padBody = await getPadBody(page);
19
- await expect(padBody).toBeVisible();
20
- });
21
-
22
- test('plugin singleton is exposed on pad.plugins after init', async ({page}) => {
23
- // Plugin's postAceInit assigns pad.plugins.ep_comments_page once
24
- // its init() promise resolves. Confirms the load path doesn't
25
- // throw and the singleton API the rest of the plugin (and its
26
- // sister ep_comments_page_admin etc.) relies on is reachable.
27
- await expect.poll(async () => page.evaluate(() => {
28
- const w = window as any;
29
- return !!(w.pad && w.pad.plugins && w.pad.plugins.ep_comments_page);
30
- }), {timeout: 15_000}).toBe(true);
31
- });
32
- });