fix(magazine): correct flagged words in place; whole-text editor keeps the paragraph's look and underlines
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dd5ddc5842
commit
119d3ba3ae
@ -6,6 +6,9 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
---
|
---
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
- **Fixing flagged words in the review list no longer restyles the paragraph or hides the underlines.** Clicking an underlined word now edits just that word in place (a small field in the paragraph's own type, suggestion chips, Enter applies, Esc cancels, *Keep* for names), through the same checked one-word fix as the spelling list. Clicking elsewhere in the text opens the whole-text editor, which now keeps the paragraph's font size, line height, width and wrapping (it was 13 px in a full-row box against 14 px text in a narrower column), grows with its text instead of scrolling in a small box, and keeps the doubtful words underlined while you type. On a phone the row text and editors are both 16 px (what iOS needs to avoid zooming in).
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -397,7 +397,12 @@ where the magazine's licence allows.
|
|||||||
|
|
||||||
**Purpose.** Fix what OCR got wrong and always be able to see, and undo, what you changed.
|
**Purpose.** Fix what OCR got wrong and always be able to see, and undo, what you changed.
|
||||||
|
|
||||||
**Steps.** In Review, click a region's text (or its pencil) and press *Save text*. A region you edited shows an
|
**Fix a flagged word where it stands.** Words the spelling check doubts are underlined in the region's text. Click one (or Tab to it
|
||||||
|
and press Enter): only that word turns into a small field, in the same type as the paragraph, so nothing around it moves or
|
||||||
|
changes. Type the correction and press **Enter**, or click one of the suggested words; **Esc** puts the word back; **Keep**
|
||||||
|
stops flagging a name or technical term in this issue. Each fix can be undone with Undo, like every other correction.
|
||||||
|
|
||||||
|
**Steps.** In Review, click a region's text away from an underlined word (or its pencil) to edit the whole text in place: it keeps the paragraph's type, width and line breaks, grows with what you type, and the doubtful words stay underlined (an underline goes as soon as the word is changed). Press *Save text*. A region you edited shows an
|
||||||
**Edited** mark. *Show changes* opens a word-by-word comparison with the text as first recognised: words removed are
|
**Edited** mark. *Show changes* opens a word-by-word comparison with the text as first recognised: words removed are
|
||||||
struck through and marked with a minus, words added are underlined and marked with a plus (never colour alone).
|
struck through and marked with a minus, words added are underlined and marked with a plus (never colour alone).
|
||||||
*Restore recognised text* puts the original back. Typing the original back yourself also clears the mark.
|
*Restore recognised text* puts the original back. Typing the original back yourself also clears the mark.
|
||||||
|
|||||||
@ -3020,6 +3020,71 @@ async function magRowRestore(id) {
|
|||||||
magReloadReview('Recognised text restored');
|
magReloadReview('Recognised text restored');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Correct a flagged word where it stands ───────────────────────────────────
|
||||||
|
// The word becomes a small input set in the paragraph's own type (nothing around it reflows or restyles), with
|
||||||
|
// the dictionary's suggestions as one-click chips. Enter applies, Esc puts the word back. The fix goes through
|
||||||
|
// the same checked route as the spelling list (offsets re-verified server-side, one Undo).
|
||||||
|
function magWordEdit(blockId, start, end, ev) {
|
||||||
|
if (ev) { ev.preventDefault(); ev.stopPropagation(); }
|
||||||
|
const issues = state.spell?.issues || [];
|
||||||
|
const n = issues.findIndex((i) => i.block_id === blockId && i.start === start && i.end === end);
|
||||||
|
const row = document.querySelector('#mag-review [data-block-row="' + CSS.escape(blockId) + '"]');
|
||||||
|
const mark = row?.querySelector('mark[data-spell-start="' + Number(start) + '"]');
|
||||||
|
if (n < 0 || !mark || mark.querySelector('input')) return;
|
||||||
|
const issue = issues[n];
|
||||||
|
const original = issue.raw || issue.token;
|
||||||
|
const shown = mark.textContent;
|
||||||
|
mark.classList.add('is-editing');
|
||||||
|
mark.removeAttribute('role');
|
||||||
|
mark.innerHTML = '<input type="text" class="mag-word-input" maxlength="120" autocomplete="off" spellcheck="false" ' +
|
||||||
|
'aria-label="Correct the word ' + esc(issue.token) + '" value="' + esc(original) + '" style="width:' + Math.max(3, original.length + 1) + 'ch">' +
|
||||||
|
'<span class="mag-word-pop" role="group" aria-label="Corrections for ' + esc(issue.token) + '">' +
|
||||||
|
(issue.suggestions || []).slice(0, 5).map((sug, k) => '<button type="button" class="btn-sm" data-sug="' + k + '">' + esc(sug) + '</button>').join('') +
|
||||||
|
'<button type="button" class="btn-sm" data-keep title="This word is fine (a name or technical term): stop flagging it in this issue">Keep</button>' +
|
||||||
|
'<span class="mag-word-hint muted">Enter applies · Esc cancels</span></span>';
|
||||||
|
const input = mark.querySelector('input');
|
||||||
|
const restore = () => { mark.classList.remove('is-editing'); mark.setAttribute('role', 'button'); mark.textContent = shown; };
|
||||||
|
const apply = (value) => {
|
||||||
|
const v = String(value).replace(/\s+/g, ' ').trim();
|
||||||
|
if (!v) return say('Type the corrected word first', 'err');
|
||||||
|
if (v === original) return restore();
|
||||||
|
magSpellApply(n, v);
|
||||||
|
};
|
||||||
|
['click', 'keyup'].forEach((t) => mark.addEventListener(t, (e) => { if (mark.classList.contains('is-editing')) e.stopPropagation(); }));
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); apply(input.value); }
|
||||||
|
else if (e.key === 'Escape') { e.preventDefault(); restore(); }
|
||||||
|
});
|
||||||
|
input.addEventListener('input', () => { input.style.width = Math.max(3, input.value.length + 1) + 'ch'; });
|
||||||
|
mark.querySelector('.mag-word-pop').addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('button');
|
||||||
|
if (!btn) return;
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
if (btn.dataset.keep !== undefined) return magSpellKeep(n);
|
||||||
|
apply(issue.suggestions[Number(btn.dataset.sug)]);
|
||||||
|
});
|
||||||
|
input.addEventListener('blur', () => setTimeout(() => {
|
||||||
|
if (mark.isConnected && mark.classList.contains('is-editing') && !mark.contains(document.activeElement)) restore();
|
||||||
|
}, 150));
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The flagged words of a block as a regex over its *current* text, so underlines survive typing: a word stops
|
||||||
|
// being marked the moment it no longer matches, with no stale offsets.
|
||||||
|
function magMarkTokens(text, tokens) {
|
||||||
|
const words = [...new Set((tokens || []).filter(Boolean))].sort((a, b) => b.length - a.length);
|
||||||
|
if (!words.length) return esc(text);
|
||||||
|
const re = new RegExp('(?<![\\p{L}\\p{N}])(?:' + words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')(?![\\p{L}\\p{N}])', 'gu');
|
||||||
|
let out = '', pos = 0, m;
|
||||||
|
while ((m = re.exec(text))) {
|
||||||
|
out += esc(text.slice(pos, m.index)) + '<mark class="mag-spell">' + esc(m[0]) + '</mark>';
|
||||||
|
pos = m.index + m[0].length;
|
||||||
|
}
|
||||||
|
return out + esc(text.slice(pos));
|
||||||
|
}
|
||||||
|
|
||||||
async function magRowEdit(id) {
|
async function magRowEdit(id) {
|
||||||
if (state.reloadPending) await state.reloadPending;
|
if (state.reloadPending) await state.reloadPending;
|
||||||
const block = (state.layout?.blocks || []).find((b) => b.id === id);
|
const block = (state.layout?.blocks || []).find((b) => b.id === id);
|
||||||
@ -3029,7 +3094,8 @@ async function magRowEdit(id) {
|
|||||||
span.hidden = true;
|
span.hidden = true;
|
||||||
const editor = document.createElement('span');
|
const editor = document.createElement('span');
|
||||||
editor.className = 'mag-row-editor';
|
editor.className = 'mag-row-editor';
|
||||||
editor.innerHTML = '<textarea aria-label="Text of this region" rows="5"></textarea>' +
|
editor.innerHTML = '<span class="mag-hl-wrap"><span class="mag-hl-back" aria-hidden="true"></span>' +
|
||||||
|
'<textarea class="mag-hl-area" aria-label="Text of this region" rows="1" spellcheck="false"></textarea></span>' +
|
||||||
'<span class="mag-row-editor-actions">' +
|
'<span class="mag-row-editor-actions">' +
|
||||||
'<button type="button" class="btn-sm btn-primary" data-act="save">Save text</button>' +
|
'<button type="button" class="btn-sm btn-primary" data-act="save">Save text</button>' +
|
||||||
'<button type="button" class="btn-sm" data-act="split" title="Split this region into two where the cursor is">Split here</button>' +
|
'<button type="button" class="btn-sm" data-act="split" title="Split this region into two where the cursor is">Split here</button>' +
|
||||||
@ -3037,6 +3103,16 @@ async function magRowEdit(id) {
|
|||||||
span.after(editor);
|
span.after(editor);
|
||||||
const area = editor.querySelector('textarea');
|
const area = editor.querySelector('textarea');
|
||||||
area.value = block.text;
|
area.value = block.text;
|
||||||
|
// Set in the paragraph's own type, growing with its text, with the flagged words still underlined while you type.
|
||||||
|
const back = editor.querySelector('.mag-hl-back');
|
||||||
|
const tokens = ((state.spellByBlock && state.spellByBlock.get(id)) || []).map((i) => i.raw || i.token);
|
||||||
|
const sync = () => {
|
||||||
|
back.innerHTML = magMarkTokens(area.value, tokens) + (area.value.endsWith('\n') ? '\n ' : '');
|
||||||
|
area.style.height = 'auto';
|
||||||
|
area.style.height = area.scrollHeight + 'px';
|
||||||
|
};
|
||||||
|
area.addEventListener('input', sync);
|
||||||
|
sync();
|
||||||
['click', 'keydown', 'keyup'].forEach((ev) => editor.addEventListener(ev, (e) => e.stopPropagation()));
|
['click', 'keydown', 'keyup'].forEach((ev) => editor.addEventListener(ev, (e) => e.stopPropagation()));
|
||||||
area.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); magReloadReview(); } });
|
area.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); magReloadReview(); } });
|
||||||
editor.addEventListener('click', async (e) => {
|
editor.addEventListener('click', async (e) => {
|
||||||
@ -4044,7 +4120,12 @@ function magSpellMarked(blockId, text) {
|
|||||||
issues.filter((i) => i.end <= text.length).sort((x, y) => x.start - y.start).forEach((i) => {
|
issues.filter((i) => i.end <= text.length).sort((x, y) => x.start - y.start).forEach((i) => {
|
||||||
if (i.start < pos) return;
|
if (i.start < pos) return;
|
||||||
const hint = 'Possible misspelling' + (i.suggestions.length ? ' — try: ' + i.suggestions.join(', ') : '');
|
const hint = 'Possible misspelling' + (i.suggestions.length ? ' — try: ' + i.suggestions.join(', ') : '');
|
||||||
out += esc(text.slice(pos, i.start)) + '<mark class="mag-spell" title="' + esc(hint) + '">' +
|
// In a review row a flagged word is a control: click it to correct just that word, in place.
|
||||||
|
const live = state.view === 'review' && i.block_id === blockId;
|
||||||
|
out += esc(text.slice(pos, i.start)) + '<mark class="mag-spell' + (live ? ' mag-spell-edit' : '') + '" title="' + esc(hint + (live ? ' — click to correct' : '')) + '"' +
|
||||||
|
(live ? ' role="button" tabindex="0" data-spell-start="' + Number(i.start) + '" data-spell-end="' + Number(i.end) + '"' +
|
||||||
|
' onclick="magWordEdit(\'' + esc(blockId) + '\', ' + Number(i.start) + ', ' + Number(i.end) + ', event)"' +
|
||||||
|
' onkeydown="if(event.key===\'Enter\'||event.key===\' \'){magWordEdit(\'' + esc(blockId) + '\', ' + Number(i.start) + ', ' + Number(i.end) + ', event);}"' : '') + '>' +
|
||||||
esc(text.slice(i.start, i.end)) + '</mark>';
|
esc(text.slice(i.start, i.end)) + '</mark>';
|
||||||
pos = i.end;
|
pos = i.end;
|
||||||
});
|
});
|
||||||
@ -5653,6 +5734,8 @@ root.magRunPageQuality = magRunPageQuality;
|
|||||||
root.magFixReadingOrder = magFixReadingOrder;
|
root.magFixReadingOrder = magFixReadingOrder;
|
||||||
root.magCleanUpBoxes = magCleanUpBoxes;
|
root.magCleanUpBoxes = magCleanUpBoxes;
|
||||||
root.magRowEdit = magRowEdit;
|
root.magRowEdit = magRowEdit;
|
||||||
|
root.magWordEdit = magWordEdit;
|
||||||
|
root.magMarkTokens = magMarkTokens;
|
||||||
root.magRowDiff = magRowDiff;
|
root.magRowDiff = magRowDiff;
|
||||||
root.magRowRestore = magRowRestore;
|
root.magRowRestore = magRowRestore;
|
||||||
root.magWordDiff = magWordDiff;
|
root.magWordDiff = magWordDiff;
|
||||||
|
|||||||
@ -5328,7 +5328,9 @@ input[list="lang-codes"] {
|
|||||||
/* The type picker and delete button stay on small screens; the text drops to its own line. */
|
/* The type picker and delete button stay on small screens; the text drops to its own line. */
|
||||||
.mag-block-type { grid-column: 3; }
|
.mag-block-type { grid-column: 3; }
|
||||||
.mag-block-type > .mdi { display: none; }
|
.mag-block-type > .mdi { display: none; }
|
||||||
.mag-block-text, .mag-block-thumb-wrap { grid-column: 1 / -1; }
|
.mag-block-text, .mag-block-thumb-wrap, .mag-row-editor { grid-column: 1 / -1; }
|
||||||
|
/* iOS zooms into any field under 16px, so on a phone the paragraph is 16px too: the editors then match it exactly. */
|
||||||
|
.mag-block-text { font-size: 16px; }
|
||||||
.mag-article { font-size: 18px; }
|
.mag-article { font-size: 18px; }
|
||||||
.mag-driving-controls { gap: 20px; }
|
.mag-driving-controls { gap: 20px; }
|
||||||
.mag-driving-btn { width: 76px; height: 76px; font-size: 28px; }
|
.mag-driving-btn { width: 76px; height: 76px; font-size: 28px; }
|
||||||
@ -5366,7 +5368,7 @@ input[list="lang-codes"] {
|
|||||||
.mag-row-actions { display: inline-flex; gap: 2px; grid-column: -2 / -1; justify-self: end; }
|
.mag-row-actions { display: inline-flex; gap: 2px; grid-column: -2 / -1; justify-self: end; }
|
||||||
.mag-row-action { padding: 2px 6px; min-width: 28px; min-height: 28px; color: var(--subtext); }
|
.mag-row-action { padding: 2px 6px; min-width: 28px; min-height: 28px; color: var(--subtext); }
|
||||||
.mag-row-action:hover, .mag-row-action:focus-visible { color: var(--text); }
|
.mag-row-action:hover, .mag-row-action:focus-visible { color: var(--text); }
|
||||||
.mag-row-editor { display: flex; flex-direction: column; gap: 6px; grid-column: 1 / -1; }
|
.mag-row-editor { display: flex; flex-direction: column; gap: 6px; min-width: 0; } /* the text's own column, so the wrapping does not change */
|
||||||
.mag-row-editor textarea { width: 100%; font: inherit; font-size: 13px; padding: 6px; border: 1px solid var(--border);
|
.mag-row-editor textarea { width: 100%; font: inherit; font-size: 13px; padding: 6px; border: 1px solid var(--border);
|
||||||
border-radius: var(--radius); background: var(--surface); color: var(--text); resize: vertical; }
|
border-radius: var(--radius); background: var(--surface); color: var(--text); resize: vertical; }
|
||||||
.mag-row-editor-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
.mag-row-editor-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
@ -5625,3 +5627,21 @@ details.mag-report-group summary { cursor: pointer; margin-bottom: 6px; }
|
|||||||
border: 1px solid var(--border); border-radius: var(--radius); overflow-wrap: anywhere; }
|
border: 1px solid var(--border); border-radius: var(--radius); overflow-wrap: anywhere; }
|
||||||
.mag-cat-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; align-items: end; }
|
.mag-cat-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; align-items: end; }
|
||||||
.mag-cat-form label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
|
.mag-cat-form label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
|
||||||
|
|
||||||
|
/* Editing a region's text in place: same type and wrapping as the displayed paragraph, flagged words still underlined */
|
||||||
|
.mag-hl-wrap { position: relative; display: block; font-size: 14px; line-height: 1.45; }
|
||||||
|
.mag-hl-back, .mag-hl-area { font: inherit; line-height: inherit; letter-spacing: inherit; white-space: pre-wrap; overflow-wrap: break-word;
|
||||||
|
word-break: normal; padding: 0; margin: 0; border: 0; width: 100%; box-sizing: border-box; }
|
||||||
|
.mag-hl-back { position: absolute; inset: 0; color: transparent; pointer-events: none; overflow: hidden; }
|
||||||
|
.mag-hl-back .mag-spell { color: transparent; }
|
||||||
|
.mag-row-editor textarea.mag-hl-area { position: relative; display: block; font: inherit; font-size: inherit; line-height: inherit; padding: 0; border: 0; width: 100%; background: transparent; color: var(--text); resize: none; overflow: hidden;
|
||||||
|
min-height: 1.45em; border-radius: 4px; outline: none; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent), 0 0 0 5px var(--accent); }
|
||||||
|
/* One flagged word being corrected where it stands */
|
||||||
|
.mag-spell.is-editing { text-decoration: none; background: none; outline: none; position: relative; cursor: text; }
|
||||||
|
.mag-word-input { font: inherit; line-height: inherit; color: var(--text); background: var(--surface); padding: 0 2px; margin: 0; min-width: 3ch; max-width: 100%;
|
||||||
|
border: 0; border-radius: 3px; box-shadow: 0 0 0 2px var(--accent); }
|
||||||
|
.mag-word-pop { position: absolute; left: 0; top: 100%; z-index: 40; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-top: 4px; padding: 6px;
|
||||||
|
min-width: 220px; max-width: min(420px, 80vw); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, .25); font-size: 13px; font-style: normal; line-height: 1.3; }
|
||||||
|
.mag-word-hint { flex: 1 1 100%; font-size: 12px; }
|
||||||
|
@media (max-width: 860px) { .mag-hl-wrap { font-size: 16px; } } /* after the desktop rule above, so it wins */
|
||||||
|
|||||||
@ -1857,3 +1857,135 @@ test('a refused catalogue shows the server reason and keeps the form', async ({
|
|||||||
await expect(page.locator('#mag-cat-status')).toContainText('Put the login in the username field');
|
await expect(page.locator('#mag-cat-status')).toContainText('Put the login in the username field');
|
||||||
await expect(page.locator('#mag-cat-url')).toHaveValue('http://me:pw@nas.local/opds');
|
await expect(page.locator('#mag-cat-url')).toHaveValue('http://me:pw@nas.local/opds');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Correcting flagged words in the review list', () => {
|
||||||
|
const ISSUES = [
|
||||||
|
{ token: 'technology', raw: 'technology', block_id: 'b3', page: 3, start: 8, end: 18, language: 'en', suggestions: ['technologie', 'technologies'], context: 'Battery technology is entering' },
|
||||||
|
{ token: 'mature', raw: 'mature', block_id: 'b3', page: 3, start: 67, end: 73, language: 'en', suggestions: [], context: 'years to mature.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function open(page) {
|
||||||
|
await mockBackend(page);
|
||||||
|
const posts = [];
|
||||||
|
await page.route(`**/api/magazine/${DOC_ID}/spelling/dictionary`, route => { posts.push({ keep: route.request().postDataJSON() }); return route.fulfill({ json: { ok: true } }); });
|
||||||
|
await page.route(`**/api/magazine/${DOC_ID}/spelling/fix`, route => { posts.push({ fix: route.request().postDataJSON() }); return route.fulfill({ json: { ok: true, undo: 'tok', text: 'x' } }); });
|
||||||
|
await page.route(`**/api/magazine/${DOC_ID}/spelling?**`, route => route.fulfill({ json: { issues: posts.some(p => p.fix || p.keep) ? [] : ISSUES, dictionary: {} } }));
|
||||||
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
|
||||||
|
await navTo(page, 's-magazine');
|
||||||
|
await page.locator('#mag-library-list .mag-card').first().click();
|
||||||
|
await page.locator('#mag-contents-view-toggle').click();
|
||||||
|
await page.locator('.mag-card-wrap').filter({ hasText: 'Page 3' }).getByRole('button', { name: /^Review/ }).click();
|
||||||
|
await page.waitForSelector('#mag-undo-btn');
|
||||||
|
await expect(page.locator('[data-block-row="b3"] mark.mag-spell')).toHaveCount(2);
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
const fontOf = (page, sel) => page.locator(sel).first().evaluate(el => { const c = getComputedStyle(el); return `${c.fontSize}/${c.lineHeight}/${c.fontFamily}`; });
|
||||||
|
|
||||||
|
test('clicking a flagged word edits only that word, in the paragraph\'s own type, and leaves the other marks', async ({ page }) => {
|
||||||
|
await open(page);
|
||||||
|
const before = await fontOf(page, '[data-block-row="b3"] .mag-block-text');
|
||||||
|
const box = await page.locator('[data-block-row="b3"] .mag-block-text').boundingBox();
|
||||||
|
await page.locator('[data-block-row="b3"] mark.mag-spell').first().click();
|
||||||
|
const input = page.locator('[data-block-row="b3"] .mag-word-input');
|
||||||
|
await expect(input).toBeFocused();
|
||||||
|
await expect(input).toHaveValue('technology');
|
||||||
|
await expect(page.locator('[data-block-row="b3"] .mag-row-editor')).toHaveCount(0); // not the whole-text editor
|
||||||
|
await expect(page.locator('[data-block-row="b3"] mark.mag-spell:not(.is-editing)')).toHaveCount(1); // the other word stays underlined
|
||||||
|
expect(await fontOf(page, '[data-block-row="b3"] .mag-block-text')).toBe(before); // nothing restyled
|
||||||
|
expect(await fontOf(page, '[data-block-row="b3"] .mag-word-input')).toBe(before);
|
||||||
|
const after = await page.locator('[data-block-row="b3"] .mag-block-text').boundingBox();
|
||||||
|
expect(Math.abs(after.width - box.width)).toBeLessThan(2); // the column did not change width
|
||||||
|
await expect(page.locator('[data-block-row="b3"] input[type="checkbox"]')).toBeChecked(); // the click did not untick the region
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a suggestion chip corrects the word with one click', async ({ page }) => {
|
||||||
|
const posts = await open(page);
|
||||||
|
await page.locator('[data-block-row="b3"] mark.mag-spell').first().click();
|
||||||
|
await page.locator('[data-block-row="b3"] .mag-word-pop').getByRole('button', { name: 'technologie', exact: true }).click();
|
||||||
|
await expect.poll(() => posts.find(p => p.fix)?.fix).toMatchObject({ block_id: 'b3', start: 8, end: 18, token: 'technology', replacement: 'technologie' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('typing a correction and pressing Enter applies it; Escape puts the word back and sends nothing', async ({ page }) => {
|
||||||
|
const posts = await open(page);
|
||||||
|
const mark = page.locator('[data-block-row="b3"] mark.mag-spell').first();
|
||||||
|
await mark.click();
|
||||||
|
const input = page.locator('[data-block-row="b3"] .mag-word-input');
|
||||||
|
await input.fill('Technologie');
|
||||||
|
await input.press('Escape');
|
||||||
|
await expect(page.locator('[data-block-row="b3"] .mag-word-input')).toHaveCount(0);
|
||||||
|
await expect(page.locator('[data-block-row="b3"] mark.mag-spell').first()).toHaveText('technology');
|
||||||
|
expect(posts).toEqual([]);
|
||||||
|
await page.locator('[data-block-row="b3"] mark.mag-spell').first().click();
|
||||||
|
await page.locator('[data-block-row="b3"] .mag-word-input').fill('Technologie');
|
||||||
|
await page.locator('[data-block-row="b3"] .mag-word-input').press('Enter');
|
||||||
|
await expect.poll(() => posts.find(p => p.fix)?.fix).toMatchObject({ token: 'technology', replacement: 'Technologie', start: 8, end: 18 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an empty or unchanged word is not sent', async ({ page }) => {
|
||||||
|
const posts = await open(page);
|
||||||
|
await page.locator('[data-block-row="b3"] mark.mag-spell').first().click();
|
||||||
|
const input = page.locator('[data-block-row="b3"] .mag-word-input');
|
||||||
|
await input.fill(' ');
|
||||||
|
await input.press('Enter');
|
||||||
|
await expect(input).toBeVisible(); // still editing: nothing to apply
|
||||||
|
await input.fill('technology');
|
||||||
|
await input.press('Enter');
|
||||||
|
await expect(page.locator('[data-block-row="b3"] .mag-word-input')).toHaveCount(0);
|
||||||
|
expect(posts).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Keep adds the word to the issue dictionary', async ({ page }) => {
|
||||||
|
const posts = await open(page);
|
||||||
|
await page.locator('[data-block-row="b3"] mark.mag-spell').nth(1).click();
|
||||||
|
await page.locator('[data-block-row="b3"] .mag-word-pop').getByRole('button', { name: 'Keep' }).click();
|
||||||
|
await expect.poll(() => posts.find(p => p.keep)?.keep).toEqual({ word: 'mature' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a flagged word can be reached and opened with the keyboard', async ({ page }) => {
|
||||||
|
await open(page);
|
||||||
|
const mark = page.locator('[data-block-row="b3"] mark.mag-spell').first();
|
||||||
|
await mark.focus();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await expect(page.locator('[data-block-row="b3"] .mag-word-input')).toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the whole-text editor keeps the paragraph\'s type, width and the flagged words underlined', async ({ page }) => {
|
||||||
|
await open(page);
|
||||||
|
const textSel = '[data-block-row="b3"] .mag-block-text';
|
||||||
|
const before = await fontOf(page, textSel);
|
||||||
|
const box = await page.locator(textSel).boundingBox();
|
||||||
|
// Click plain text, away from any flagged word.
|
||||||
|
await page.locator(textSel).click({ position: { x: box.width - 4, y: 4 } });
|
||||||
|
const area = page.locator('[data-block-row="b3"] .mag-hl-area');
|
||||||
|
await expect(area).toBeFocused();
|
||||||
|
expect(await fontOf(page, '[data-block-row="b3"] .mag-hl-area')).toBe(before);
|
||||||
|
const editorBox = await area.boundingBox();
|
||||||
|
expect(Math.abs(editorBox.width - box.width)).toBeLessThan(3); // same column, same wrapping
|
||||||
|
await expect(page.locator('[data-block-row="b3"] .mag-hl-back mark.mag-spell')).toHaveCount(2);
|
||||||
|
// Fix one word by typing: its underline goes, the other stays.
|
||||||
|
await area.fill('Battery technologie is entering a new phase. It will take years to mature.');
|
||||||
|
await expect(page.locator('[data-block-row="b3"] .mag-hl-back mark.mag-spell')).toHaveCount(1);
|
||||||
|
await expect(page.locator('[data-block-row="b3"] .mag-hl-back mark.mag-spell')).toHaveText('mature');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the editor grows with its text instead of scrolling inside a small box', async ({ page }) => {
|
||||||
|
await open(page);
|
||||||
|
await page.locator('[data-block-row="b3"] .mag-block-text').click({ position: { x: 4, y: 4 } });
|
||||||
|
const area = page.locator('[data-block-row="b3"] .mag-hl-area');
|
||||||
|
const h1 = (await area.boundingBox()).height;
|
||||||
|
await area.fill(Array.from({ length: 40 }, (_, i) => 'Zeile ' + i + ' mit etwas mehr Text').join(' '));
|
||||||
|
await expect.poll(async () => (await area.boundingBox()).height).toBeGreaterThan(h1 * 3);
|
||||||
|
expect(await area.evaluate(el => el.scrollHeight <= el.clientHeight + 1)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Save text still saves the edited text', async ({ page }) => {
|
||||||
|
await open(page);
|
||||||
|
const patches = [];
|
||||||
|
await page.route(`**/api/magazine/${DOC_ID}/pages/3/regions/b3`, route => { patches.push(route.request().postDataJSON()); route.fulfill({ json: { ok: true } }); });
|
||||||
|
await page.locator('[data-block-row="b3"] .mag-block-text').click({ position: { x: 4, y: 4 } });
|
||||||
|
await page.locator('[data-block-row="b3"] .mag-hl-area').fill('Neuer Text.');
|
||||||
|
await page.locator('[data-block-row="b3"]').getByRole('button', { name: 'Save text' }).click();
|
||||||
|
await expect.poll(() => patches[0]).toEqual({ text: 'Neuer Text.' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user