ep_comments_page 11.0.28 → 11.0.30
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 +2 -0
- package/locales/ko.json +2 -0
- package/package.json +1 -1
- package/static/css/comment.css +14 -0
- package/static/js/index.js +5 -0
- package/static/tests/backend/specs/l10n.js +38 -0
- package/static/tests/frontend-new/helper/comments.ts +51 -5
- package/static/tests/frontend-new/specs/commentDelete.spec.ts +71 -0
- package/static/tests/frontend-new/{parked → specs}/commentEdit.spec.ts +28 -19
- package/static/tests/frontend-new/{parked → specs}/commentReply.spec.ts +2 -2
- package/static/tests/frontend-new/{parked → specs}/commentSuggestion.spec.ts +8 -4
- package/static/tests/frontend-new/{parked → specs}/comment_l10n.spec.ts +18 -3
- package/static/tests/frontend-new/{parked → specs}/comment_settings.spec.ts +1 -1
- package/static/tests/frontend-new/{parked → specs}/preCommentMark.spec.ts +1 -1
- package/static/tests/frontend-new/{parked → specs}/timeFormat.spec.ts +13 -7
- package/templates/comments.html +6 -4
- package/static/tests/frontend-new/parked/commentDelete.spec.ts +0 -57
- package/static/tests/frontend-new/specs/smoke.spec.ts +0 -32
- /package/static/tests/frontend-new/{parked → specs}/commentIcons.spec.ts +0 -0
- /package/static/tests/frontend-new/{parked → specs}/newComment.spec.ts +0 -0
- /package/static/tests/frontend-new/{parked → specs}/xcommentCopyPaste.spec.ts +0 -0
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
package/static/css/comment.css
CHANGED
|
@@ -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;
|
package/static/js/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
191
|
+
await replyForm.locator('.label-suggestion-checkbox').first().click();
|
|
161
192
|
if (suggestionText !== undefined) {
|
|
162
|
-
await
|
|
193
|
+
await replyForm.locator('textarea.to-value').first().fill(suggestionText);
|
|
163
194
|
}
|
|
164
195
|
}
|
|
165
|
-
await
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 .
|
|
91
|
+
const t = await outer.locator('.comment-container .comment-title-wrapper .to-value')
|
|
88
92
|
.first().textContent();
|
|
89
|
-
return t
|
|
90
|
-
}).toBe(
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/templates/comments.html
CHANGED
|
@@ -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.
|
|
31
|
-
<span class="
|
|
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="
|
|
102
|
-
<span class="
|
|
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
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|