diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2ec8a84d..1b691d955 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,11 @@ ones are marked like "v1.0.0-fork".
### Fixed
+* **The "edit term" button in the review table went nowhere** (#266): it pointed
+ at `edit_tword.php`, a filename that stopped being routed in v3, and opened it
+ in a frame the reader no longer renders. It now links to `/word/edit-term`,
+ the same route the Alpine review view already used.
+
* **A single-language install could never browse Gutenberg or GDL**: the
new-text page said "Please select a language above" while the navbar already
showed one. `currentlanguage` is only written when the user picks from the
@@ -126,6 +131,16 @@ ones are marked like "v1.0.0-fork".
PHP 8.5 surfaced by deprecating `ord()` on multi-byte strings. No API or
styling change.
+### Removed
+
+* **Dead legacy result-view plumbing** (#266): four result handlers
+ (`delete_result`, `insert_wellknown_result`, `insert_ignore_result`,
+ `delete_multi_result`) outlived the views that once fed them, and
+ `word_status_ajax.ts` waited on a `#word-status-config` element no page has
+ emitted since the frame reader was retired. Dropping them plus their now
+ orphaned DOM helpers removes ~1000 lines. No behaviour change — none of it
+ could run.
+
## [3.2.1-fork] - 2026-06-30
### Fixed
diff --git a/ROADMAP.md b/ROADMAP.md
index b9fa4d1b2..31f7974ab 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -57,7 +57,7 @@ pay for the hard parts when demand proves them worth it.
## The keystone constraint
-LWT renders HTML server-side (94 view templates). Until more logic moves to the
+LWT renders HTML server-side (93 view templates). Until more logic moves to the
frontend against `/api/v1`, *any* mobile wrapper is just a window to a server.
So **"more frontend, less server-rendered" is the enabling work** — the thing
that makes a good client today and offline/local-first possible tomorrow.
@@ -93,7 +93,12 @@ that makes a good client today and offline/local-first possible tomorrow.
`textReader.renderTextContent()` against `/api/v1`, with an offline prototype
(`offline-text-reader.ts` + IndexedDB) started. `review_desktop.php` is "all UI
rendered by Alpine.js." So Phase 1 is **cutting the server-shell umbilical, not
-converting pages**. Three dependencies to sever + one real conversion:
+converting pages**.
+
+**Status (re-audited 2026-08-05): every mobile-critical surface is shell-free.**
+The only open item is the legacy-fragment cleanup in the last bullet, which is
+not a mobile blocker. Tracked on the issue board as
+[#266](https://github.com/HugoFara/lwt/issues/266).
- [x] **(Phase 0 gate) Injectable API base URL.** Done in Phase 0 — same seam.
`@shared/api/client` resolves an injectable **absolute** server root and
@@ -109,9 +114,18 @@ converting pages**. Three dependencies to sever + one real conversion:
`audioPlayer` fetch), so `read_desktop.php` carries no per-text data and
`TextReadController` dropped the server-side book/media plumbing. Verified
live (book nav, audio reveal, no-regression plain read). Toolbar labels use
- `__e()` but resolve via the client i18n `t()`. *Remaining (lower urgency,
- not reader-specific):* the **global navbar** is still PHP-rendered — tracked
- separately since it spans every page.
+ `__e()` but resolve via the client i18n `t()`. Nothing outstanding — the
+ global navbar was the last shared dependency and it shipped (next bullet).
+- [x] **Global navbar — shell-free.** Shipped in 3.2.0 (`d6e6a5203`).
+ `PageLayoutHelper::buildNavbar()` is gone; PHP now emits only the mount
+ point `
`
+ (`buildNavbarPlaceholder()`), and `mountNavbar()` fetches
+ `GET /api/v1/navbar` (per-user language list, current language, theme
+ state, admin/multi-user flags) and builds the markup in
+ `shared/components/navbar_renderer.ts`. Labels resolve from the i18n
+ bundle, so the payload stays locale-agnostic. Zero server-rendered navbar
+ markup remains in `src/**/*.php`. This was the last chrome dependency
+ spanning every page.
- [x] **i18n → client (delivery mechanism).** Shipped: `GET /api/v1/i18n[/{locale}]`
(public; `Translator::getAllTranslations()`) returns the flat
"namespace.key" => string bundle, merging English fallback — the same shape
@@ -135,8 +149,8 @@ converting pages**. Three dependencies to sever + one real conversion:
and the destructive **bulk archive/delete** now go through
`PUT /api/v1/texts/bulk-action` (per-user scoped) instead of a same-origin
form POST, so they work against a configurable API base. The remaining
- bulk actions (tag / review / reparse) intentionally stay on the form path —
- they need pickers/navigation and are desktop-admin, not mobile flows.
+ bulk actions (tag / review / reparse) intentionally stay on the form
+ path — they need pickers/navigation and are desktop-admin, not mobile.
`__e()` labels resolve via the i18n API once a page boots from it.
- [~] **Vocabulary mgmt — mobile path already shell-free; legacy fragments
remain.** *Re-audit (corrected):* the **modern reader's** word actions
@@ -153,6 +167,10 @@ converting pages**. Three dependencies to sever + one real conversion:
those popups onto the API button family / `word_modal`** and then deleting
the fragments — a UI-consolidation pass that needs live E2E in the reader,
not a server-vs-client data conversion. Track as cleanup; low urgency.
+ Still open as of 2026-08-05: eight `*_result.php` views survive
+ (`save_result`, `edit_result`, `edit_term_result`, `edit_multi_update_result`,
+ `hover_save_result`, `all_wellknown_result`, `bulk_save_result`,
+ `upload_result`) plus `Book/Views/import_result.php`.
**Out of Phase 1** (leave server-rendered, fine in a WebView online): imports
(file/web/youtube/whisper), admin/settings, language config, feeds.
@@ -176,15 +194,17 @@ the next achievable target. See `lukaisu/ROADMAP.md` for the build-side detail.
persisted in native Preferences, then navigates the WebView to the server.
- [~] **Bundled-client (Model B) build mode.** `npm run build:app`
(`vite.app.config.ts` → `dist-app/`) emits standalone connect/library/
- reader pages that boot `main.ts` against a remote `/api/v1` with no PHP in
- the loop. Each page body is **prerendered from the real PHP view** at build
- time (`build/php-view-prerender.mjs`) so the Alpine scaffolds, icons and
- labels stay a single source of truth. `../lukaisu` consumes `dist-app/` as
- its Capacitor `webDir` (`npm run sync:model-b` there). First slice: connect
- → library → reader. **CORS is now required** — the bundle origin
- (`https://localhost`) is cross-origin to every server, so servers must set
- `CORS_ALLOWED_ORIGINS=https://localhost`. Remaining: review surface,
- client-rendered global navbar, on-device QA.
+ reader/review pages that boot `main.ts` against a remote `/api/v1` with no
+ PHP in the loop. Each page body is **prerendered from the real PHP view**
+ at build time (`build/php-view-prerender.mjs`) so the Alpine scaffolds,
+ icons and labels stay a single source of truth. `../lukaisu` consumes
+ `dist-app/` as
+ its Capacitor `webDir` (`npm run sync:model-b` there). **CORS is now
+ required** — the bundle origin (`https://localhost`) is cross-origin to
+ every server, so servers must set `CORS_ALLOWED_ORIGINS=https://localhost`.
+ All four surfaces are bundled: connect → library → reader → **review**
+ (`f1f062c24`), and the navbar they share is client-rendered too.
+ **Remaining: on-device QA only** — everything else in this bullet ships.
- [ ] Reuse existing PWA assets; close the small gaps: real adaptive/maskable
launcher icons (replace placeholders) and a manifest **`id`** (Lukaisu v0.2).
- [ ] Ship through **our own F-Droid repo first** (low bar, full control,
diff --git a/src/Modules/Review/Views/table_review_row.php b/src/Modules/Review/Views/table_review_row.php
index 32b00bf53..e4673b127 100644
--- a/src/Modules/Review/Views/table_review_row.php
+++ b/src/Modules/Review/Views/table_review_row.php
@@ -65,8 +65,7 @@
?>
|
-
+
` blob that the matching
+ * initializer below reads and applies to the reading frame's DOM. The full set of
+ * views still using this mechanism:
+ *
+ * - save_result.php (new word saved in multi-word context)
* - edit_result.php (word created or updated)
+ * - edit_term_result.php (word updated during review)
+ * - edit_multi_update_result.php (multi-word expression updated)
+ * - hover_save_result.php (word saved via hover)
* - all_wellknown_result.php (mark all as well-known/ignored)
+ * - bulk_save_result.php (bulk translated words saved)
+ *
+ * Keep this list in sync with the views — a handler with no emitting view is dead
+ * code, which is how the delete/insert-wellknown/insert-ignore/delete-multi
+ * handlers came to be removed.
*
* @license Unlicense
* @since 3.0.0
@@ -18,11 +30,7 @@ import {
getParentContext,
updateLearnStatus,
updateTestWordInDOM,
- deleteWordFromDOM,
- markWordWellKnownInDOM,
- markWordIgnoredInDOM,
updateMultiWordInDOM,
- deleteMultiWordFromDOM,
updateBulkWordInDOM,
type BulkWordUpdateParams
} from '../services/word_dom_updates';
@@ -109,35 +117,6 @@ interface HoverSaveResultConfig {
todoContent: string;
}
-/**
- * Configuration for delete_result view (word deleted).
- */
-interface DeleteResultConfig {
- wid: number;
- term: string;
- todoContent: string;
-}
-
-/**
- * Configuration for insert_wellknown_result view (word marked as well-known).
- */
-interface InsertWellKnownResultConfig {
- wid: number;
- hex: string;
- term: string;
- todoContent: string;
-}
-
-/**
- * Configuration for insert_ignore_result view (word marked as ignored).
- */
-interface InsertIgnoreResultConfig {
- wid: number;
- hex: string;
- term: string;
- todoContent: string;
-}
-
/**
* Configuration for edit_multi_update_result view (multi-word expression updated).
*/
@@ -150,15 +129,6 @@ interface EditMultiUpdateResultConfig {
oldStatus: number;
}
-/**
- * Configuration for delete_multi_result view (multi-word expression deleted).
- */
-interface DeleteMultiResultConfig {
- wid: number;
- showAll: boolean;
- todoContent: string;
-}
-
/**
* Configuration for bulk_save_result view (bulk translated words saved).
*/
@@ -320,33 +290,6 @@ function initEditResult(config: EditResultConfig): void {
completeWordOperation(config.todoContent);
}
-/**
- * Initialize delete_result view.
- * Updates the DOM after a word is deleted (reset to unknown state).
- */
-function initDeleteResult(config: DeleteResultConfig): void {
- deleteWordFromDOM(config.wid, config.term);
- completeWordOperation(config.todoContent);
-}
-
-/**
- * Initialize insert_wellknown_result view.
- * Updates the DOM after marking a word as well-known (status 99).
- */
-function initInsertWellKnownResult(config: InsertWellKnownResultConfig): void {
- markWordWellKnownInDOM(config.wid, config.hex, config.term);
- completeWordOperation(config.todoContent);
-}
-
-/**
- * Initialize insert_ignore_result view.
- * Updates the DOM after marking a word as ignored (status 98).
- */
-function initInsertIgnoreResult(config: InsertIgnoreResultConfig): void {
- markWordIgnoredInDOM(config.wid, config.hex, config.term);
- completeWordOperation(config.todoContent);
-}
-
/**
* Initialize edit_multi_update_result view.
* Updates the DOM after a multi-word expression is updated.
@@ -363,15 +306,6 @@ function initEditMultiUpdateResult(config: EditMultiUpdateResultConfig): void {
cleanupRightFrames();
}
-/**
- * Initialize delete_multi_result view.
- * Updates the DOM after a multi-word expression is deleted.
- */
-function initDeleteMultiResult(config: DeleteMultiResultConfig): void {
- deleteMultiWordFromDOM(config.wid, config.showAll);
- completeWordOperation(config.todoContent);
-}
-
/**
* Initialize bulk_save_result view.
* Updates the DOM after bulk translated words are saved.
@@ -455,39 +389,6 @@ export function autoInitWordResults(): void {
}
}
- // Delete result
- const deleteConfigEl = document.querySelector('script[data-lwt-delete-result-config]');
- if (deleteConfigEl) {
- try {
- const config = JSON.parse(deleteConfigEl.textContent || '{}') as DeleteResultConfig;
- initDeleteResult(config);
- } catch (e) {
- console.error('Failed to parse delete result config:', e);
- }
- }
-
- // Insert well-known result
- const insertWellKnownConfigEl = document.querySelector('script[data-lwt-insert-wellknown-result-config]');
- if (insertWellKnownConfigEl) {
- try {
- const config = JSON.parse(insertWellKnownConfigEl.textContent || '{}') as InsertWellKnownResultConfig;
- initInsertWellKnownResult(config);
- } catch (e) {
- console.error('Failed to parse insert wellknown result config:', e);
- }
- }
-
- // Insert ignore result
- const insertIgnoreConfigEl = document.querySelector('script[data-lwt-insert-ignore-result-config]');
- if (insertIgnoreConfigEl) {
- try {
- const config = JSON.parse(insertIgnoreConfigEl.textContent || '{}') as InsertIgnoreResultConfig;
- initInsertIgnoreResult(config);
- } catch (e) {
- console.error('Failed to parse insert ignore result config:', e);
- }
- }
-
// Edit multi-word update result
const editMultiUpdateConfigEl = document.querySelector('script[data-lwt-edit-multi-update-result-config]');
if (editMultiUpdateConfigEl) {
@@ -499,17 +400,6 @@ export function autoInitWordResults(): void {
}
}
- // Delete multi-word result
- const deleteMultiConfigEl = document.querySelector('script[data-lwt-delete-multi-result-config]');
- if (deleteMultiConfigEl) {
- try {
- const config = JSON.parse(deleteMultiConfigEl.textContent || '{}') as DeleteMultiResultConfig;
- initDeleteMultiResult(config);
- } catch (e) {
- console.error('Failed to parse delete multi result config:', e);
- }
- }
-
// Bulk save result
const bulkSaveConfigEl = document.querySelector('script[data-lwt-bulk-save-result-config]');
if (bulkSaveConfigEl) {
diff --git a/src/frontend/js/modules/vocabulary/services/word_dom_updates.ts b/src/frontend/js/modules/vocabulary/services/word_dom_updates.ts
index 1ad3db3e1..dd59927f8 100644
--- a/src/frontend/js/modules/vocabulary/services/word_dom_updates.ts
+++ b/src/frontend/js/modules/vocabulary/services/word_dom_updates.ts
@@ -149,33 +149,6 @@ export function updateWordStatusInDOM(
});
}
-/**
- * Delete a word from the DOM (reset to unknown/status0 state).
- *
- * @param wid Word ID
- * @param term Term text
- */
-export function deleteWordFromDOM(wid: number, term: string): void {
- const context = getParentContext();
-
- context.querySelectorAll(`.word${wid}`).forEach(elem => {
- const ann = elem.getAttribute('data_ann') ?? '';
- const trans = elem.getAttribute('data_trans') ?? '';
- const rom = elem.getAttribute('data_rom') ?? '';
- const combinedTrans = ann + (ann ? ' / ' : '') + trans;
- const title = createWordTooltip(term, combinedTrans, rom, '0');
-
- elem.classList.remove('status99', 'status98', 'status1', 'status2', 'status3', 'status4', 'status5', `word${wid}`);
- elem.classList.add('status0');
- elem.setAttribute('data_status', '0');
- elem.setAttribute('data_trans', '');
- elem.setAttribute('data_rom', '');
- elem.setAttribute('data_wid', '');
- elem.title = title;
- elem.removeAttribute('data_img');
- });
-}
-
/**
* Mark a word as well-known (status 99) in the DOM.
*
@@ -251,44 +224,6 @@ export function updateMultiWordInDOM(
});
}
-/**
- * Delete a multi-word expression from the DOM.
- *
- * @param wid Word ID
- * @param showAll Whether to show all words (affects visibility of sub-words)
- */
-export function deleteMultiWordFromDOM(wid: number, showAll: boolean): void {
- const context = getParentContext();
-
- context.querySelectorAll(`.word${wid}`).forEach(wordEl => {
- const sid = wordEl.parentElement;
- wordEl.remove();
-
- if (!showAll && sid) {
- // Show all hidden elements
- sid.querySelectorAll('*').forEach(el => {
- el.classList.remove('hide');
- });
-
- // Re-hide elements based on multi-word expression rules
- sid.querySelectorAll('.mword').forEach(mword => {
- if (!mword.classList.contains('hide')) {
- const code = parseInt(mword.getAttribute('data_code') ?? '0', 10);
- const order = parseInt(mword.getAttribute('data_order') ?? '0', 10);
- const u = code * 2 + order - 1;
-
- // Hide all siblings until we find the end marker
- let sibling = mword.nextElementSibling as HTMLElement | null;
- while (sibling && !sibling.id?.startsWith(`ID-${u}-`)) {
- sibling.classList.add('hide');
- sibling = sibling.nextElementSibling as HTMLElement | null;
- }
- }
- });
- }
- });
-}
-
export interface BulkWordUpdateParams {
WoID: number;
WoTextLC: string;
@@ -326,35 +261,6 @@ export function updateBulkWordInDOM(term: BulkWordUpdateParams, useTooltip: bool
});
}
-/**
- * Update word for hover save operation.
- *
- * @param wid Word ID
- * @param hex Hex class identifier
- * @param status Word status
- * @param translation Translation text
- * @param wordRaw Raw word text
- */
-export function updateHoverSaveInDOM(
- wid: number,
- hex: string,
- status: number | string,
- translation: string,
- wordRaw: string
-): void {
- const context = getParentContext();
- const title = createWordTooltip(wordRaw, translation, '', String(status));
-
- context.querySelectorAll(`[data_hex="${hex}"]`).forEach(el => {
- el.classList.remove('status0');
- el.classList.add(`status${status}`, `word${wid}`);
- el.setAttribute('data_status', String(status));
- el.setAttribute('data_wid', String(wid));
- el.title = title;
- el.setAttribute('data_trans', translation);
- });
-}
-
/**
* Update word data attributes for test result views.
*
diff --git a/src/frontend/js/modules/vocabulary/services/word_status_ajax.ts b/src/frontend/js/modules/vocabulary/services/word_status_ajax.ts
deleted file mode 100644
index d4e469222..000000000
--- a/src/frontend/js/modules/vocabulary/services/word_status_ajax.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * Word Status AJAX - Functions for updating word status via AJAX
- *
- * Handles status change requests and DOM updates for the reading interface.
- *
- * @license Unlicense
- * @since 3.0.0
- */
-
-import { onDomReady } from '@shared/utils/dom_ready';
-import { TermsApi } from '@modules/vocabulary/api/terms_api';
-import { updateWordStatusInDOM } from '../services/word_dom_updates';
-import { cleanupRightFrames } from '@modules/text/pages/reading/frame_management';
-
-export interface WordStatusUpdateData {
- wid: number;
- status: number;
- term: string;
- translation: string;
- romanization: string;
- todoContent: string;
-}
-
-/**
- * Display error message for failed word status update.
- */
-export function wordUpdateError(): void {
- const logEl = document.getElementById('status_change_log');
- if (logEl) {
- logEl.textContent = 'Word status update failed!';
- }
- cleanupRightFrames();
-}
-
-/**
- * Apply word status update to the DOM after successful AJAX call.
- *
- * @param data Word status update data
- */
-export function applyWordUpdate(data: WordStatusUpdateData): void {
- const logEl = document.getElementById('status_change_log');
- if (logEl) {
- logEl.textContent = `Term status changed to ${data.status}`;
- }
-
- updateWordStatusInDOM(
- data.wid,
- data.status,
- data.term,
- data.translation,
- data.romanization
- );
-
- const frameH = window.parent?.document?.getElementById('frame-h');
- if (frameH) {
- const learnStatus = frameH.querySelector('#learnstatus');
- if (learnStatus) {
- learnStatus.innerHTML = data.todoContent;
- }
- }
-
- cleanupRightFrames();
-}
-
-/**
- * Send AJAX request to update word status.
- *
- * @param data Word status update data
- */
-export async function updateWordStatusAjax(data: WordStatusUpdateData): Promise {
- const response = await TermsApi.setStatus(data.wid, data.status);
-
- if (response.error) {
- wordUpdateError();
- } else {
- applyWordUpdate(data);
- }
-}
-
-/**
- * Initialize word status change from result view.
- * Called from status_result.php after page load.
- *
- * @param config Configuration object with word data
- */
-export function initWordStatusChange(config: WordStatusUpdateData): void {
- updateWordStatusAjax(config);
-}
-
-/**
- * Auto-initialize word status change from JSON config element.
- * Reads configuration from #word-status-config and triggers the update.
- */
-function autoInitWordStatusChange(): void {
- const configEl = document.getElementById('word-status-config');
- if (!configEl) {
- return;
- }
-
- try {
- const config: WordStatusUpdateData = JSON.parse(configEl.textContent || '{}');
- initWordStatusChange(config);
- } catch {
- // Config parse failed, page may not be status result
- }
-}
-
-// Auto-initialize on DOM ready
-onDomReady(autoInitWordStatusChange);
diff --git a/src/frontend/js/shared/utils/simple_interactions.ts b/src/frontend/js/shared/utils/simple_interactions.ts
index 5637dbe80..5391a3949 100644
--- a/src/frontend/js/shared/utils/simple_interactions.ts
+++ b/src/frontend/js/shared/utils/simple_interactions.ts
@@ -197,11 +197,6 @@ export function initSimpleInteractions(): void {
}
break;
- case 'show-right-frames':
- // Legacy action - right frames panel was removed in v3.2.0
- // This action is now a no-op
- break;
-
case 'hide-right-frames':
// Legacy action - right frames panel was removed in v3.2.0
// This action is now a no-op
diff --git a/tests/frontend/words/word_dom_updates.test.ts b/tests/frontend/words/word_dom_updates.test.ts
index af6aa05df..affc1fbf1 100644
--- a/tests/frontend/words/word_dom_updates.test.ts
+++ b/tests/frontend/words/word_dom_updates.test.ts
@@ -10,13 +10,10 @@ import {
updateNewWordInDOM,
updateExistingWordInDOM,
updateWordStatusInDOM,
- deleteWordFromDOM,
markWordWellKnownInDOM,
markWordIgnoredInDOM,
updateMultiWordInDOM,
- deleteMultiWordFromDOM,
updateBulkWordInDOM,
- updateHoverSaveInDOM,
updateTestWordInDOM,
completeWordOperation,
type WordUpdateParams,
@@ -299,48 +296,6 @@ describe('word_dom_updates.ts', () => {
});
});
- // ===========================================================================
- // deleteWordFromDOM Tests
- // ===========================================================================
-
- describe('deleteWordFromDOM', () => {
- it('resets word to status0', () => {
- document.body.innerHTML = `
- word
- `;
-
- deleteWordFromDOM(789, 'word');
-
- const element = document.querySelector('span')!;
- expect(element.classList.contains('word789')).toBe(false);
- expect(element.classList.contains('status3')).toBe(false);
- expect(element.classList.contains('status0')).toBe(true);
- expect(element.getAttribute('data_status')).toBe('0');
- expect(element.getAttribute('data_trans')).toBe('');
- expect(element.getAttribute('data_rom')).toBe('');
- expect(element.getAttribute('data_wid')).toBe('');
- expect(element.getAttribute('data_img')).toBeNull();
- });
-
- it('removes all status classes', () => {
- document.body.innerHTML = `
- word
- `;
-
- deleteWordFromDOM(789, 'word');
-
- const element = document.querySelector('span')!;
- expect(element.classList.contains('status99')).toBe(false);
- expect(element.classList.contains('status98')).toBe(false);
- expect(element.classList.contains('status1')).toBe(false);
- expect(element.classList.contains('status2')).toBe(false);
- expect(element.classList.contains('status3')).toBe(false);
- expect(element.classList.contains('status4')).toBe(false);
- expect(element.classList.contains('status5')).toBe(false);
- expect(element.classList.contains('status0')).toBe(true);
- });
- });
-
// ===========================================================================
// markWordWellKnownInDOM Tests
// ===========================================================================
@@ -418,42 +373,6 @@ describe('word_dom_updates.ts', () => {
});
});
- // ===========================================================================
- // deleteMultiWordFromDOM Tests
- // ===========================================================================
-
- describe('deleteMultiWordFromDOM', () => {
- it('removes multi-word elements and shows sub-words', () => {
- document.body.innerHTML = `
-
- hello world
- hello
- world
-
- `;
-
- deleteMultiWordFromDOM(444, false);
-
- expect(document.querySelectorAll('.word444').length).toBe(0);
- expect(document.querySelectorAll('.hide').length).toBe(0);
- });
-
- it('removes multi-word elements when showAll is true', () => {
- document.body.innerHTML = `
-
- hello world
- hello
-
- `;
-
- deleteMultiWordFromDOM(444, true);
-
- expect(document.querySelectorAll('.word444').length).toBe(0);
- // When showAll is true, hidden elements are not unhidden
- expect(document.querySelectorAll('.hide').length).toBe(1);
- });
- });
-
// ===========================================================================
// updateBulkWordInDOM Tests
// ===========================================================================
@@ -501,38 +420,6 @@ describe('word_dom_updates.ts', () => {
});
});
- // ===========================================================================
- // updateHoverSaveInDOM Tests
- // ===========================================================================
-
- describe('updateHoverSaveInDOM', () => {
- it('updates word after hover save operation', () => {
- document.body.innerHTML = `
- hello
- `;
-
- updateHoverSaveInDOM(666, '48454c4c4f', 1, 'quick trans', 'hello');
-
- const element = document.querySelector('[data_hex="48454c4c4f"]')!;
- expect(element.classList.contains('status0')).toBe(false);
- expect(element.classList.contains('status1')).toBe(true);
- expect(element.classList.contains('word666')).toBe(true);
- expect(element.getAttribute('data_trans')).toBe('quick trans');
- expect(element.getAttribute('data_wid')).toBe('666');
- });
-
- it('sets title with generated tooltip', () => {
- document.body.innerHTML = `
- hello
- `;
-
- updateHoverSaveInDOM(666, '48454c4c4f', 1, 'quick trans', 'hello');
-
- // Title is set with formatted tooltip
- expect(document.querySelector('[data_hex="48454c4c4f"]')!.getAttribute('title')).toBe('hello|quick trans||1');
- });
- });
-
// ===========================================================================
// updateTestWordInDOM Tests
// ===========================================================================
@@ -604,18 +491,6 @@ describe('word_dom_updates.ts', () => {
expect(document.querySelector('.word888')!.getAttribute('data_status')).toBe('99');
});
- it('handles missing annotation data in deleteWordFromDOM', () => {
- document.body.innerHTML = `
- word
- `;
-
- deleteWordFromDOM(999, 'word');
-
- // Should not throw even without data_ann attribute
- expect(document.querySelectorAll('.word999').length).toBe(0);
- expect(document.querySelectorAll('.status0').length).toBe(1);
- });
-
it('handles multiple elements with same word ID', () => {
document.body.innerHTML = `
test
diff --git a/tests/frontend/words/word_result_init.test.ts b/tests/frontend/words/word_result_init.test.ts
index 4a5bcdcc4..5644c9bb7 100644
--- a/tests/frontend/words/word_result_init.test.ts
+++ b/tests/frontend/words/word_result_init.test.ts
@@ -12,11 +12,7 @@ vi.mock('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates',
getParentContext: vi.fn(() => document),
updateLearnStatus: vi.fn(),
updateTestWordInDOM: vi.fn(),
- deleteWordFromDOM: vi.fn(),
- markWordWellKnownInDOM: vi.fn(),
- markWordIgnoredInDOM: vi.fn(),
updateMultiWordInDOM: vi.fn(),
- deleteMultiWordFromDOM: vi.fn(),
updateBulkWordInDOM: vi.fn()
}));
@@ -39,11 +35,7 @@ vi.mock('../../../src/frontend/js/shared/utils/html_utils', () => ({
import {
updateNewWordInDOM,
completeWordOperation,
- deleteWordFromDOM,
- markWordWellKnownInDOM,
- markWordIgnoredInDOM,
updateMultiWordInDOM,
- deleteMultiWordFromDOM,
updateBulkWordInDOM,
updateLearnStatus,
updateExistingWordInDOM
@@ -189,77 +181,6 @@ describe('word_result_init.ts', () => {
});
});
- // ===========================================================================
- // Delete Result Config Tests
- // ===========================================================================
-
- describe('delete result config', () => {
- it('initializes from delete result config', () => {
- document.body.innerHTML = `
-
- `;
-
- autoInitWordResults();
-
- expect(deleteWordFromDOM).toHaveBeenCalledWith(111, 'deleted word');
- expect(completeWordOperation).toHaveBeenCalledWith('8 words');
- });
- });
-
- // ===========================================================================
- // Insert Well-Known Result Config Tests
- // ===========================================================================
-
- describe('insert wellknown result config', () => {
- it('initializes from insert wellknown result config', () => {
- document.body.innerHTML = `
-
- `;
-
- autoInitWordResults();
-
- expect(markWordWellKnownInDOM).toHaveBeenCalledWith(222, 'aaa', 'known word');
- expect(completeWordOperation).toHaveBeenCalledWith('3 words');
- });
- });
-
- // ===========================================================================
- // Insert Ignore Result Config Tests
- // ===========================================================================
-
- describe('insert ignore result config', () => {
- it('initializes from insert ignore result config', () => {
- document.body.innerHTML = `
-
- `;
-
- autoInitWordResults();
-
- expect(markWordIgnoredInDOM).toHaveBeenCalledWith(333, 'bbb', 'ignored word');
- expect(completeWordOperation).toHaveBeenCalledWith('2 words');
- });
- });
-
// ===========================================================================
// Edit Multi Update Result Config Tests
// ===========================================================================
@@ -287,29 +208,6 @@ describe('word_result_init.ts', () => {
});
});
- // ===========================================================================
- // Delete Multi Result Config Tests
- // ===========================================================================
-
- describe('delete multi result config', () => {
- it('initializes from delete multi result config', () => {
- document.body.innerHTML = `
-
- `;
-
- autoInitWordResults();
-
- expect(deleteMultiWordFromDOM).toHaveBeenCalledWith(555, true);
- expect(completeWordOperation).toHaveBeenCalledWith('12 words');
- });
- });
-
// ===========================================================================
// Bulk Save Result Config Tests
// ===========================================================================
@@ -522,19 +420,25 @@ describe('word_result_init.ts', () => {
it('processes multiple different config types', () => {
document.body.innerHTML = `
-
-
`;
autoInitWordResults();
expect(cleanupRightFrames).toHaveBeenCalled();
- expect(deleteWordFromDOM).toHaveBeenCalled();
- expect(markWordWellKnownInDOM).toHaveBeenCalled();
+ expect(updateNewWordInDOM).toHaveBeenCalled();
+ expect(updateMultiWordInDOM).toHaveBeenCalled();
});
});
diff --git a/tests/frontend/words/word_status_ajax.test.ts b/tests/frontend/words/word_status_ajax.test.ts
deleted file mode 100644
index b4e4fb851..000000000
--- a/tests/frontend/words/word_status_ajax.test.ts
+++ /dev/null
@@ -1,454 +0,0 @@
-/**
- * Tests for word_status_ajax.ts - AJAX word status updates
- */
-import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-import {
- wordUpdateError,
- applyWordUpdate,
- updateWordStatusAjax,
- initWordStatusChange,
- type WordStatusUpdateData
-} from '../../../src/frontend/js/modules/vocabulary/services/word_status_ajax';
-
-// Mock dependencies
-vi.mock('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates', () => ({
- updateWordStatusInDOM: vi.fn(),
- updateLearnStatus: vi.fn()
-}));
-
-vi.mock('../../../src/frontend/js/modules/text/pages/reading/frame_management', () => ({
- cleanupRightFrames: vi.fn()
-}));
-
-describe('word_status_ajax.ts', () => {
- beforeEach(() => {
- document.body.innerHTML = '';
- vi.clearAllMocks();
- // Reset parent window mock
- delete (window as any).parent;
- });
-
- afterEach(() => {
- vi.restoreAllMocks();
- document.body.innerHTML = '';
- });
-
- // ===========================================================================
- // wordUpdateError Tests
- // ===========================================================================
-
- describe('wordUpdateError', () => {
- it('displays error message in status_change_log', async () => {
- const { cleanupRightFrames } = await import('../../../src/frontend/js/modules/text/pages/reading/frame_management');
-
- document.body.innerHTML = `
-
- `;
-
- wordUpdateError();
-
- expect(document.querySelector('#status_change_log')!.textContent).toBe('Word status update failed!');
- expect(cleanupRightFrames).toHaveBeenCalled();
- });
-
- it('calls cleanupRightFrames even when element does not exist', async () => {
- const { cleanupRightFrames } = await import('../../../src/frontend/js/modules/text/pages/reading/frame_management');
-
- document.body.innerHTML = '';
-
- wordUpdateError();
-
- expect(cleanupRightFrames).toHaveBeenCalled();
- });
- });
-
- // ===========================================================================
- // applyWordUpdate Tests
- // ===========================================================================
-
- describe('applyWordUpdate', () => {
- it('displays success message with status', async () => {
- const { updateWordStatusInDOM } = await import('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates');
- const { cleanupRightFrames } = await import('../../../src/frontend/js/modules/text/pages/reading/frame_management');
-
- document.body.innerHTML = `
-
- `;
-
- const data: WordStatusUpdateData = {
- wid: 123,
- status: 3,
- term: 'hello',
- translation: 'bonjour',
- romanization: '',
- todoContent: '5 words'
- };
-
- applyWordUpdate(data);
-
- expect(document.querySelector('#status_change_log')!.textContent).toBe('Term status changed to 3');
- expect(updateWordStatusInDOM).toHaveBeenCalledWith(
- 123,
- 3,
- 'hello',
- 'bonjour',
- ''
- );
- expect(cleanupRightFrames).toHaveBeenCalled();
- });
-
- it('updates learnstatus in parent frame-h', async () => {
- await import('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates');
-
- document.body.innerHTML = `
-
- `;
-
- // Create a mock frame-h with learnstatus
- const mockFrameH = document.createElement('div');
- mockFrameH.id = 'frame-h';
- mockFrameH.innerHTML = 'old content ';
- document.body.appendChild(mockFrameH);
-
- (window as any).parent = {
- document: document
- };
-
- const data: WordStatusUpdateData = {
- wid: 456,
- status: 4,
- term: 'test',
- translation: 'prueba',
- romanization: 'pɾweβa',
- todoContent: 'New content'
- };
-
- applyWordUpdate(data);
-
- expect(mockFrameH.querySelector('#learnstatus')!.innerHTML).toBe('New content');
- });
-
- it('handles missing frame-h gracefully', async () => {
- const { updateWordStatusInDOM } = await import('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates');
-
- document.body.innerHTML = `
-
- `;
-
- (window as any).parent = {
- document: document
- };
-
- const data: WordStatusUpdateData = {
- wid: 789,
- status: 5,
- term: 'word',
- translation: 'mot',
- romanization: '',
- todoContent: 'content'
- };
-
- // Should not throw
- expect(() => applyWordUpdate(data)).not.toThrow();
- expect(updateWordStatusInDOM).toHaveBeenCalled();
- });
- });
-
- // ===========================================================================
- // updateWordStatusAjax Tests
- // ===========================================================================
-
- describe('updateWordStatusAjax', () => {
- it('makes POST request to correct endpoint', () => {
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 100,
- status: 2,
- term: 'test',
- translation: 'trans',
- romanization: 'rom',
- todoContent: 'todo'
- };
-
- updateWordStatusAjax(data);
-
- // Note: The actual implementation might use fetch or XMLHttpRequest
- // This test verifies the endpoint structure
- });
-
- it('calls applyWordUpdate on successful response', async () => {
- await import('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates');
-
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 200,
- status: 3,
- term: 'word',
- translation: 'mot',
- romanization: '',
- todoContent: 'content'
- };
-
- updateWordStatusAjax(data);
-
- // The implementation should call updateWordStatusInDOM
- // This might need to wait for async operations
- });
-
- it('calls wordUpdateError on empty response', async () => {
- await import('../../../src/frontend/js/modules/text/pages/reading/frame_management');
-
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve(''),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 300,
- status: 4,
- term: 'test',
- translation: 'trans',
- romanization: '',
- todoContent: 'todo'
- };
-
- updateWordStatusAjax(data);
-
- // Error handling for empty response
- });
-
- it('calls wordUpdateError on error response', async () => {
- await import('../../../src/frontend/js/modules/text/pages/reading/frame_management');
-
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ error: 'Something went wrong' }),
- ok: false
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 400,
- status: 5,
- term: 'test',
- translation: 'trans',
- romanization: '',
- todoContent: 'todo'
- };
-
- updateWordStatusAjax(data);
-
- // Error handling for error response
- });
- });
-
- // ===========================================================================
- // initWordStatusChange Tests
- // ===========================================================================
-
- describe('initWordStatusChange', () => {
- it('calls updateWordStatusAjax with config', () => {
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const config: WordStatusUpdateData = {
- wid: 500,
- status: 1,
- term: 'word',
- translation: 'translation',
- romanization: 'romanization',
- todoContent: 'todo'
- };
-
- initWordStatusChange(config);
-
- // Should initiate status change
- });
- });
-
- // ===========================================================================
- // Edge Cases
- // ===========================================================================
-
- describe('Edge Cases', () => {
- it('handles status 98 (ignored)', () => {
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 600,
- status: 98,
- term: 'the',
- translation: '*',
- romanization: '',
- todoContent: 'todo'
- };
-
- updateWordStatusAjax(data);
-
- // Should handle ignored status
- });
-
- it('handles status 99 (well-known)', () => {
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 700,
- status: 99,
- term: 'hello',
- translation: '*',
- romanization: '',
- todoContent: 'todo'
- };
-
- updateWordStatusAjax(data);
-
- // Should handle well-known status
- });
-
- it('handles empty term and translation', () => {
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 800,
- status: 1,
- term: '',
- translation: '',
- romanization: '',
- todoContent: ''
- };
-
- expect(() => updateWordStatusAjax(data)).not.toThrow();
- });
-
- it('handles special characters in term', async () => {
- await import('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates');
-
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 900,
- status: 2,
- term: "l'école",
- translation: 'the school',
- romanization: '',
- todoContent: 'todo'
- };
-
- updateWordStatusAjax(data);
-
- // Should handle special characters
- });
-
- it('handles Unicode characters in term', async () => {
- await import('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates');
-
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- const data: WordStatusUpdateData = {
- wid: 1000,
- status: 3,
- term: '日本語',
- translation: 'Japanese',
- romanization: 'nihongo',
- todoContent: 'todo'
- };
-
- updateWordStatusAjax(data);
-
- // Should handle Unicode characters
- });
-
- it('handles null parent document gracefully', async () => {
- const { updateWordStatusInDOM } = await import('../../../src/frontend/js/modules/vocabulary/services/word_dom_updates');
-
- global.fetch = vi.fn(() =>
- Promise.resolve({
- json: () => Promise.resolve({ success: true }),
- ok: true
- } as Response)
- ) as any;
-
- document.body.innerHTML = ``;
-
- (window as any).parent = {
- document: null
- };
-
- const data: WordStatusUpdateData = {
- wid: 1100,
- status: 4,
- term: 'test',
- translation: 'trans',
- romanization: '',
- todoContent: 'content'
- };
-
- // Should not throw
- expect(() => applyWordUpdate(data)).not.toThrow();
- expect(updateWordStatusInDOM).toHaveBeenCalled();
- });
- });
-});
|