Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions __mocks__/platform-bible-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ export const MOCK_SAVE_AS_MENU_ITEM: MenuItemContainingCommand = {
localizeNotes: '',
};

/** Sentinel menu item passed by the mock toolbar when the wipe button is clicked. */
export const MOCK_WIPE_MENU_ITEM: MenuItemContainingCommand = {
label: '%interlinearizer_wipe%',
command: 'interlinearizer.wipe',
/** Sentinel menu item passed by the mock toolbar when the discard-changes button is clicked. */
export const MOCK_DISCARD_CHANGES_MENU_ITEM: MenuItemContainingCommand = {
label: '%interlinearizer_discardChanges%',
command: 'interlinearizer.openDiscardChangesModal',
group: 'interlinearizer.draftActions',
order: 1,
localizeNotes: '',
Expand Down Expand Up @@ -259,10 +259,10 @@ export function TabToolbar({
{onSelectProjectMenuItem && (
<button
type="button"
data-testid="tab-toolbar-wipe"
onClick={() => onSelectProjectMenuItem(MOCK_WIPE_MENU_ITEM)}
data-testid="tab-toolbar-discard-changes"
onClick={() => onSelectProjectMenuItem(MOCK_DISCARD_CHANGES_MENU_ITEM)}
>
Wipe
Discard Changes
</button>
)}
{onSelectProjectMenuItem && (
Expand Down
27 changes: 14 additions & 13 deletions contributions/localizedStrings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"%interlinearizer_openProjectInfoModal%": "View Project Info…",
"%interlinearizer_save%": "Save",
"%interlinearizer_saveAs%": "Save As…",
"%interlinearizer_wipe%": "Wipe…",
"%interlinearizer_discardChanges%": "Discard Unsaved Changes…",
"%interlinearizer_openLexiconChooser%": "Connect a Lexicon…",
"%interlinearizer_error_openLexiconChooser_failed%": "The lexicon chooser could not be opened.",

Expand All @@ -32,10 +32,10 @@
"%interlinearizer_redone_catalogDeleteChecked%": "Redid deleting {count} analyses ({usageCount} uses).",
"%interlinearizer_undone_catalogClearGlossChecked%": "Undid clearing the gloss on {count} analyses.",
"%interlinearizer_redone_catalogClearGlossChecked%": "Redid clearing the gloss on {count} analyses.",
"%interlinearizer_undone_wipeBook%": "Undid wiping {book}.",
"%interlinearizer_redone_wipeBook%": "Redid wiping {book}.",
"%interlinearizer_undone_wipeAll%": "Undid wiping the whole draft.",
"%interlinearizer_redone_wipeAll%": "Redid wiping the whole draft.",
"%interlinearizer_undone_discardBook%": "Undid discarding unsaved changes to {book}.",
"%interlinearizer_redone_discardBook%": "Redid discarding unsaved changes to {book}.",
"%interlinearizer_undone_discardAll%": "Undid discarding all unsaved changes.",
"%interlinearizer_redone_discardAll%": "Redid discarding all unsaved changes.",

"%interlinearizer_menu_column_view%": "View",
"%interlinearizer_openAnalysisCatalog%": "Analysis Catalog",
Expand Down Expand Up @@ -299,14 +299,14 @@
"%interlinearizer_modal_saveAs_save_active_clean%": "No changes to save.",
"%interlinearizer_modal_saveAs_cancel%": "Cancel",

"%interlinearizer_wipe_modal_title%": "Wipe draft analysis",
"%interlinearizer_wipe_modal_prompt%": "Choose how much of the draft's analysis to remove. Save afterward to persist the change to a project.",
"%interlinearizer_wipe_modal_scope_book%": "Current book",
"%interlinearizer_wipe_modal_scope_book_description%": "Removes all analysis for the current book from the draft.",
"%interlinearizer_wipe_modal_scope_all%": "Entire draft",
"%interlinearizer_wipe_modal_scope_all_description%": "Removes all analysis from the draft.",
"%interlinearizer_wipe_modal_confirm%": "Wipe",
"%interlinearizer_wipe_modal_cancel%": "Cancel",
"%interlinearizer_discardChanges_modal_title%": "Discard unsaved changes",
"%interlinearizer_discardChanges_modal_prompt%": "Choose how much of the unsaved analyses to discard.",
"%interlinearizer_discardChanges_modal_scope_book%": "Current book",
"%interlinearizer_discardChanges_modal_scope_book_description%": "Discard all unsaved analyses for the current book.",
"%interlinearizer_discardChanges_modal_scope_all%": "Entire draft",
"%interlinearizer_discardChanges_modal_scope_all_description%": "Discard all unsaved analyses.",
"%interlinearizer_discardChanges_modal_confirm%": "Discard",
"%interlinearizer_discardChanges_modal_cancel%": "Cancel",

"%interlinearizer_bookNotInProject_title%": "Book not in this project",
"%interlinearizer_bookNotInProject_description%": "This book is not part of this project yet. You can add it with {buttonLabel}.",
Expand All @@ -324,6 +324,7 @@
"%interlinearizer_error_load_projects_failed%": "Could not load interlinear projects. Please try again.",
"%interlinearizer_error_save_draft_failed%": "Could not save your working draft. Please try again.",
"%interlinearizer_error_save_analysis_failed%": "Could not save your analysis. Please try again.",
"%interlinearizer_error_discardChanges_failed%": "The saved project could not be read, so no changes were discarded.",
"%interlinearizer_error_save_project_missing%": "The project you were saving to no longer exists. Use Save As to save your work to another project.",
"%interlinearizer_info_save_in_progress%": "A save is in progress. Try again when it finishes.",
"%interlinearizer_status_saving%": "Saving…",
Expand Down
6 changes: 3 additions & 3 deletions contributions/menus.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@
"command": "interlinearizer.openSaveAsModal"
},
{
"label": "%interlinearizer_wipe%",
"localizeNotes": "Interlinearizer top menu > Open the wipe dialog to remove the current book's or the whole draft's analysis",
"label": "%interlinearizer_discardChanges%",
"localizeNotes": "Interlinearizer top menu > Open the dialog that discards the unsaved analysis of the current book or the whole draft",
"group": "interlinearizer.draftActions",
"order": 1,
"command": "interlinearizer.wipe"
"command": "interlinearizer.openDiscardChangesModal"
},
{
"label": "%interlinearizer_openLexiconChooser%",
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Feature tests run with `npm run test:e2e:cdp`. That command launches a fresh, is
- **Import from `cdp.fixture`, never `app.fixture`.** The CDP config already serializes execution (`workers: 1`), so tests never race each other on the shared instance.
- **The instance is only ever used with the WEB project.** This is an operating assumption, not something tests verify: `ensureInterlinearizerOpenOnWeb()` trusts an existing Interlinearizer tab and only picks WEB when opening fresh. Don't point `start:cdp` at other projects.
- **Mutating tests operate on the dedicated "E2E Test Project", never on a developer's own projects.** `ensureE2eProjectActive()` opens it (creating it on first use) at the start of each mutating test. Because the draft is the single per-source working buffer, replacing it could destroy unsaved developer work — so when the draft is dirty and the active project is _not_ the e2e project, the helper first saves the draft into a new `e2e-rescued-work-<timestamp>` project. Rescue projects are backups, not junk: delete them manually once recovered. Dirty state left while the e2e project is active is treated as leftover test data and discarded.
- **Self-establish every precondition.** A mutating test's setup composes these steps as needed: `ensureInterlinearizerOpenOnWeb()`, `ensureE2eProjectActive()`, `navigateToScriptureRef()` (the scroll-group reference could be anywhere), and `wipeDraft()`. They stay as separate calls rather than one wrapper on purpose — tests pick the subset they need (e.g. the draft-persistence reopen deliberately runs only open + navigate, skipping the project reset and wipe that would destroy the state it is verifying).
- **Self-establish every precondition.** A mutating test's setup composes these steps as needed: `ensureInterlinearizerOpenOnWeb()`, `ensureE2eProjectActive()`, `navigateToScriptureRef()` (the scroll-group reference could be anywhere), and `discardAllChanges()`. They stay as separate calls rather than one wrapper on purpose — tests pick the subset they need (e.g. the draft-persistence reopen deliberately runs only open + navigate, skipping the project reset and discard that would destroy the state it is verifying).
- **Reset at the start, tidy at the end.** Correctness rests on the start-of-test sequence, which self-heals whatever a failed run left behind (leftover modals, stray project pickers, another test's dirty draft); see the JSDocs on `ensureInterlinearizerOpenOnWeb()` and `ensureE2eProjectActive()` for the mechanism. This self-healing is also what lets the CDP config safely `retries` in CI. Mutating tests additionally end with `ensureE2eProjectActive(page, { rescueDirtyDraft: false })` to discard their own leftovers — a courtesy to the next run, not something correctness depends on.
- **Use unique per-run values** (e.g. `` `e2e-gloss-${Date.now()}` ``) for anything written into the draft, so a stale leftover can never satisfy an assertion.
- **Drive only the visible UI.** No JSON-RPC/WebSocket calls to set up or assert state (the rpc.discover readiness polls in the shared helpers are the one sanctioned exception).
Expand Down
28 changes: 16 additions & 12 deletions e2e-tests/fixtures/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1695,28 +1695,32 @@ export async function ensureE2eProjectActive(
}

/**
* Wipe the entire draft's analysis via the visible UI (Project menu → Wipe… → "Entire draft" →
* Wipe). Standard reset step for mutating feature tests on the shared CDP instance: run it at the
* START of a test (never at the end) so a previously failed run self-heals instead of poisoning the
* next one.
* Discard the entire draft's unsaved changes via the visible UI (Project menu → Discard Unsaved
* Changes… → "Entire draft" → Discard). With the e2e project active, the discard empties the draft,
* because no test saves to that project. Standard reset step for mutating feature tests on the
* shared CDP instance: run it at the START of a test (never at the end) so a previously failed run
* self-heals instead of poisoning the next one.
*
* @throws If the wipe dialog does not open or does not close after confirming.
* @throws If the discard dialog does not open or does not close after confirming.
*/
export async function wipeDraft(page: Page): Promise<void> {
export async function discardAllChanges(page: Page): Promise<void> {
const frame = await openInterlinearizerProjectMenu(page);
await frame.getByRole('menuitem', { name: /Wipe/i }).first().click();
await frame
.getByRole('menuitem', { name: /Discard Unsaved Changes/i })
.first()
.click();

const wipeDialogTitle = frame.getByTestId('wipe-modal-title');
await expect(wipeDialogTitle).toBeVisible({ timeout: 5_000 });
const scopeAll = frame.getByTestId('wipe-scope-all');
const dialogTitle = frame.getByTestId('discard-changes-modal-title');
await expect(dialogTitle).toBeVisible({ timeout: 5_000 });
const scopeAll = frame.getByTestId('discard-changes-scope-all');
// `force`: on a slow/software-rendered CI display the just-opened modal overlay hasn't won the
// hit-test yet, so a normal click is intercepted by the iframe's `#root` for a few frames and
// times out. We've asserted the modal is open and target an element inside it by a unique test id,
// so skipping the hit-test check is safe.
await expect(scopeAll).toBeEnabled({ timeout: 5_000 });
await scopeAll.check({ force: true });
await frame.getByTestId('wipe-confirm').click({ force: true });
await expect(wipeDialogTitle).not.toBeVisible({ timeout: 10_000 });
await frame.getByTestId('discard-changes-confirm').click({ force: true });
await expect(dialogTitle).not.toBeVisible({ timeout: 10_000 });
}

/**
Expand Down
4 changes: 2 additions & 2 deletions e2e-tests/global-setup-cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ const RENDERER_SETTLE_TIMEOUT = process.env.CI ? 180_000 : 120_000;
* 4. Records the PID and user-data dir for {@link globalTeardownCdp}.
*
* The isolated user-data dir means the run never touches a developer's real profile; the feature
* tests self-establish every precondition (they create the E2E project, navigate, and wipe the
* draft at the start of each test), so a fresh profile is sufficient.
* tests self-establish every precondition (they create the E2E project, navigate, and discard the
* draft's changes at the start of each test), so a fresh profile is sufficient.
*
* If the CDP port is already in use, a warm instance is assumed (a developer's own `npm run
* start:cdp`) and setup is a no-op: no app is launched and nothing is recorded for teardown, so the
Expand Down
4 changes: 2 additions & 2 deletions e2e-tests/tests/features/draft-persistence.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getInterlinearizerFrame,
navigateToScriptureRef,
waitForAppAndInterlinearizerReady,
wipeDraft,
discardAllChanges,
} from '../../fixtures/helpers';

test.describe('Draft persistence', () => {
Expand All @@ -21,7 +21,7 @@ test.describe('Draft persistence', () => {
await ensureInterlinearizerOpenOnWeb(mainPage);
await ensureE2eProjectActive(mainPage);
await navigateToScriptureRef(mainPage, 'GEN 1:1');
await wipeDraft(mainPage);
await discardAllChanges(mainPage);

const frame = getInterlinearizerFrame(mainPage);
const glossInput = frame.getByLabel('Gloss for beginning', { exact: true }).first();
Expand Down
6 changes: 3 additions & 3 deletions e2e-tests/tests/features/gloss-roundtrip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
getInterlinearizerFrame,
navigateToScriptureRef,
waitForAppAndInterlinearizerReady,
wipeDraft,
discardAllChanges,
} from '../../fixtures/helpers';

test.describe('Gloss round-trip', () => {
Expand All @@ -19,14 +19,14 @@ test.describe('Gloss round-trip', () => {
await ensureInterlinearizerOpenOnWeb(mainPage);
await ensureE2eProjectActive(mainPage);
await navigateToScriptureRef(mainPage, 'GEN 1:1');
await wipeDraft(mainPage);
await discardAllChanges(mainPage);

const frame = getInterlinearizerFrame(mainPage);

// WEB Genesis 1:1: "In the beginning, God created the heavens and the earth."
const glossInput = frame.getByLabel('Gloss for beginning', { exact: true }).first();
await expect(glossInput).toBeVisible({ timeout: 30_000 });
// The wipe just cleared all analysis, so the field must start empty.
// The discard just emptied the draft, so the field must start empty.
await expect(glossInput).toHaveValue('');

// Unique per run so a leftover value from a previous run can never false-pass.
Expand Down
Loading
Loading