Skip to content

Commit e8c7b57

Browse files
Prevent double notifications
1 parent d4fdf59 commit e8c7b57

7 files changed

Lines changed: 30 additions & 81 deletions

File tree

src/__tests__/components/CreateProjectModal.test.tsx

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -210,39 +210,6 @@ describe('CreateProjectModal', () => {
210210
expect(onClose).not.toHaveBeenCalled();
211211
});
212212

213-
it('does not call onProjectCreated or onClose when sendCommand returns undefined', async () => {
214-
jest.mocked(papi.commands.sendCommand).mockResolvedValue(undefined);
215-
const onProjectCreated = jest.fn();
216-
const onClose = jest.fn();
217-
render(
218-
<CreateProjectModal
219-
projectId={testProjectId}
220-
onClose={onClose}
221-
onProjectCreated={onProjectCreated}
222-
/>,
223-
);
224-
225-
await userEvent.click(screen.getByRole('button', { name: /^create$/i }));
226-
227-
await waitFor(() => expect(papi.commands.sendCommand).toHaveBeenCalled());
228-
expect(onProjectCreated).not.toHaveBeenCalled();
229-
expect(onClose).not.toHaveBeenCalled();
230-
});
231-
232-
it('sends an error notification when sendCommand returns undefined', async () => {
233-
jest.mocked(papi.commands.sendCommand).mockResolvedValue(undefined);
234-
render(<CreateProjectModal projectId={testProjectId} onClose={jest.fn()} />);
235-
236-
await userEvent.click(screen.getByRole('button', { name: /^create$/i }));
237-
238-
await waitFor(() =>
239-
expect(papi.notifications.send).toHaveBeenCalledWith({
240-
message: '%interlinearizer_error_create_project_failed%',
241-
severity: 'error',
242-
}),
243-
);
244-
});
245-
246213
it('defaults analysis language to ["und"] when the language input contains only whitespace', async () => {
247214
render(<CreateProjectModal projectId={testProjectId} onClose={() => {}} />);
248215

@@ -264,7 +231,7 @@ describe('CreateProjectModal', () => {
264231
});
265232

266233
it('disables the create button while a submission is in progress', async () => {
267-
let resolveCommand: (value: undefined) => void = () => {};
234+
let resolveCommand: (value: string) => void = () => {};
268235
jest.mocked(papi.commands.sendCommand).mockImplementation(
269236
() =>
270237
new Promise((resolve) => {
@@ -277,13 +244,13 @@ describe('CreateProjectModal', () => {
277244
await userEvent.click(createButton);
278245

279246
expect(createButton).toBeDisabled();
280-
resolveCommand(undefined);
247+
resolveCommand('not valid json{{{');
281248

282249
await waitFor(() => expect(createButton).not.toBeDisabled());
283250
});
284251

285252
it('disables the cancel button while a submission is in progress', async () => {
286-
let resolveCommand: (value: undefined) => void = () => {};
253+
let resolveCommand: (value: string) => void = () => {};
287254
jest.mocked(papi.commands.sendCommand).mockImplementation(
288255
() =>
289256
new Promise((resolve) => {
@@ -296,7 +263,7 @@ describe('CreateProjectModal', () => {
296263
await userEvent.click(screen.getByRole('button', { name: /^create$/i }));
297264

298265
expect(cancelButton).toBeDisabled();
299-
resolveCommand(undefined);
266+
resolveCommand('not valid json{{{');
300267

301268
await waitFor(() => expect(cancelButton).not.toBeDisabled());
302269
});

src/__tests__/components/ProjectMetadataModal.test.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ describe('ProjectMetadataModal', () => {
219219
);
220220
});
221221

222-
it('does not call onProjectSaved or onClose when save sendCommand resolves with a falsy value', async () => {
222+
it('does not call onProjectSaved or onClose when save sendCommand resolves with undefined (project not found)', async () => {
223223
mockSendCommand.mockResolvedValue(undefined);
224224
const onProjectSaved = jest.fn();
225225
const onClose = jest.fn();
@@ -234,18 +234,14 @@ describe('ProjectMetadataModal', () => {
234234
expect(onClose).not.toHaveBeenCalled();
235235
});
236236

237-
it('sends an error notification when save sendCommand resolves with a falsy value', async () => {
237+
it('does not send a notification when save sendCommand resolves with undefined (project not found)', async () => {
238238
mockSendCommand.mockResolvedValue(undefined);
239239
render(<ProjectMetadataModal {...testProps} />);
240240

241241
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
242242

243-
await waitFor(() =>
244-
expect(papi.notifications.send).toHaveBeenCalledWith({
245-
message: '%interlinearizer_error_save_metadata_failed%',
246-
severity: 'error',
247-
}),
248-
);
243+
await waitFor(() => expect(mockSendCommand).toHaveBeenCalled());
244+
expect(papi.notifications.send).not.toHaveBeenCalled();
249245
});
250246

251247
it('does not call onProjectSaved, onClose, or send a notification when save sendCommand rejects', async () => {

src/__tests__/main.test.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -582,13 +582,11 @@ describe('main', () => {
582582
expect(__mockSelectProject).not.toHaveBeenCalled();
583583
});
584584

585-
it('logs the error, sends an error notification, and returns undefined when storage fails', async () => {
585+
it('logs the error, sends an error notification, and rethrows when storage fails', async () => {
586586
mockCreateProject.mockRejectedValue(new Error('disk full'));
587587
const handler = await getCreateProjectHandler();
588588

589-
const result = await handler('src-project', ['en']);
590-
591-
expect(result).toBeUndefined();
589+
await expect(handler('src-project', ['en'])).rejects.toThrow('disk full');
592590
expect(__mockLogger.error).toHaveBeenCalledWith(
593591
'Interlinearizer: failed to create project',
594592
expect.any(Error),
@@ -908,13 +906,11 @@ describe('main', () => {
908906
expect(result).toBeUndefined();
909907
});
910908

911-
it('logs the error, sends an error notification, and returns undefined when storage throws', async () => {
909+
it('logs the error, sends an error notification, and rethrows when storage throws', async () => {
912910
mockUpdateProjectMetadata.mockRejectedValue(new Error('disk full'));
913911
const handler = await getUpdateProjectMetadataHandler();
914912

915-
const result = await handler('proj-id', 'My Name', 'My Desc', ['en']);
916-
917-
expect(result).toBeUndefined();
913+
await expect(handler('proj-id', 'My Name', 'My Desc', ['en'])).rejects.toThrow('disk full');
918914
expect(__mockLogger.error).toHaveBeenCalledWith(
919915
'Interlinearizer: failed to update project metadata',
920916
expect.any(Error),

src/components/CreateProjectModal.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,6 @@ export function CreateProjectModal({
8080
name.trim() || undefined,
8181
description.trim() || undefined,
8282
);
83-
if (!projectJson) {
84-
await papi.notifications.send({
85-
message: '%interlinearizer_error_create_project_failed%',
86-
severity: 'error',
87-
});
88-
return;
89-
}
9083
const parsed: unknown = JSON.parse(projectJson);
9184
if (!isInterlinearProjectSummary(parsed)) {
9285
await papi.notifications.send({

src/components/ProjectMetadataModal.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,7 @@ export function ProjectMetadataModal({
117117
newLanguages,
118118
targetProjectId,
119119
);
120-
if (!updatedProjectJson) {
121-
await papi.notifications.send({
122-
message: '%interlinearizer_error_save_metadata_failed%',
123-
severity: 'error',
124-
});
125-
return;
126-
}
120+
if (!updatedProjectJson) return;
127121
onProjectSaved?.({
128122
name: newName,
129123
description: newDescription,

src/main.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ async function openInterlinearizerForWebView(webViewId?: string): Promise<string
111111
/**
112112
* Creates a new interlinearizer project for the given source project. Called from the WebView via
113113
* `papi.commands.sendCommand` after the user fills in the create-project modal. Returns the
114-
* persisted project serialized as a JSON string, or `undefined` if storage fails (failure is also
115-
* logged and shown as a notification).
114+
* persisted project serialized as a JSON string.
116115
*
117116
* @param sourceProjectId - Platform.Bible project ID of the source text to interlinearize.
118117
* @param analysisLanguages - BCP 47 tags for languages used in glosses and annotations (e.g.
@@ -121,16 +120,17 @@ async function openInterlinearizerForWebView(webViewId?: string): Promise<string
121120
* alignment projects. Omit for analysis-only projects.
122121
* @param name - Optional user-facing name for the project.
123122
* @param description - Optional user-facing description for the project.
124-
* @returns JSON-stringified `InterlinearProject` for the new project, or `undefined` if storage
125-
* fails.
123+
* @returns JSON-stringified `InterlinearProject` for the new project.
124+
* @throws If storage fails. The error is logged and an error notification is sent before rethrowing
125+
* so the frontend `catch` block can suppress it without sending a second notification.
126126
*/
127127
async function createInterlinearProject(
128128
sourceProjectId: string,
129129
analysisLanguages: string[],
130130
targetProjectId?: string,
131131
name?: string,
132132
description?: string,
133-
): Promise<string | undefined> {
133+
): Promise<string> {
134134
try {
135135
const project = await projectStorage.createProject(
136136
executionToken,
@@ -149,7 +149,7 @@ async function createInterlinearProject(
149149
severity: 'error',
150150
})
151151
.catch(() => {});
152-
return undefined;
152+
throw e;
153153
}
154154
}
155155

@@ -193,8 +193,9 @@ async function deleteInterlinearProject(interlinearProjectId: string): Promise<v
193193
* pass the current value to leave it unchanged.
194194
* @param targetProjectId - New target-project ID; omit to clear the target binding.
195195
* @returns JSON string of the updated `InterlinearProject`, or `undefined` if the project ID is not
196-
* found or if a storage failure occurs (in which case the error is logged and a notification is
197-
* sent to the user).
196+
* found.
197+
* @throws If storage fails. The error is logged and an error notification is sent before rethrowing
198+
* so the frontend `catch` block can suppress it without sending a second notification.
198199
*/
199200
async function updateProjectMetadata(
200201
interlinearProjectId: string,
@@ -221,7 +222,7 @@ async function updateProjectMetadata(
221222
severity: 'error',
222223
})
223224
.catch(() => {});
224-
return undefined;
225+
throw e;
225226
}
226227
}
227228

src/types/interlinearizer.d.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ declare module 'papi-shared-types' {
2424
/**
2525
* Creates a new interlinearizer project for the given source project. Called from the WebView
2626
* after the user fills in the create-project modal. Returns the persisted `InterlinearProject`
27-
* serialized as a JSON string, or `undefined` if project creation fails (failure is logged and
28-
* surfaced as an error notification).
27+
* serialized as a JSON string.
2928
*
3029
* @param sourceProjectId Platform.Bible project ID of the source text to interlinearize.
3130
* @param analysisLanguages BCP 47 tags for all languages used in glosses and annotations (e.g.
@@ -36,16 +35,17 @@ declare module 'papi-shared-types' {
3635
* Omitted for analysis-only projects (LCM, PT9 single-sided).
3736
* @param name Optional user-facing name for the project.
3837
* @param description Optional user-facing description for the project.
39-
* @returns The persisted `InterlinearProject` as a JSON string, or `undefined` if creation
40-
* fails.
38+
* @returns The persisted `InterlinearProject` as a JSON string.
39+
* @throws If storage fails. The error is logged and an error notification is sent before
40+
* rethrowing so callers do not need to send a second notification.
4141
*/
4242
'interlinearizer.createProject': (
4343
sourceProjectId: string,
4444
analysisLanguages: string[],
4545
targetProjectId?: string,
4646
name?: string,
4747
description?: string,
48-
) => Promise<string | undefined>;
48+
) => Promise<string>;
4949

5050
/**
5151
* Returns all interlinearizer projects for the given source project, serialized as a JSON
@@ -98,6 +98,8 @@ declare module 'papi-shared-types' {
9898
* target-side text binding).
9999
* @returns The updated project as a JSON string, or `undefined` if no project with that ID
100100
* exists.
101+
* @throws If storage fails. The error is logged and an error notification is sent before
102+
* rethrowing so callers do not need to send a second notification.
101103
*/
102104
'interlinearizer.updateProjectMetadata': (
103105
interlinearProjectId: string,

0 commit comments

Comments
 (0)