Skip to content

Commit c160ae3

Browse files
committed
Update test
1 parent 3e7dfc9 commit c160ae3

1 file changed

Lines changed: 257 additions & 5 deletions

File tree

src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts

Lines changed: 257 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
import { DeepnoteFile, serializeDeepnoteFile } from '@deepnote/blocks';
12
import { assert } from 'chai';
23
import * as sinon from 'sinon';
3-
import { anything, instance, mock, when } from 'ts-mockito';
4-
import { Disposable, EventEmitter, FileSystemWatcher, NotebookCellKind, NotebookDocument, Uri } from 'vscode';
4+
import { anything, instance, mock, reset, resetCalls, verify, when } from 'ts-mockito';
5+
import {
6+
Disposable,
7+
EventEmitter,
8+
FileSystemWatcher,
9+
NotebookCellKind,
10+
NotebookDocument,
11+
NotebookEdit,
12+
Uri,
13+
WorkspaceEdit
14+
} from 'vscode';
515

616
import type { IControllerRegistration } from '../controllers/types';
717
import type { IDisposableRegistry } from '../../platform/common/types';
@@ -11,7 +21,7 @@ import { IDeepnoteNotebookManager } from '../types';
1121
import { DeepnoteFileChangeWatcher } from './deepnoteFileChangeWatcher';
1222
import { SnapshotService } from './snapshots/snapshotService';
1323

14-
const validProject = {
24+
const validProject: DeepnoteFile = {
1525
version: '1.0.0',
1626
metadata: { createdAt: '2025-01-01T00:00:00Z' },
1727
project: {
@@ -21,11 +31,61 @@ const validProject = {
2131
{
2232
id: 'notebook-1',
2333
name: 'Notebook 1',
24-
blocks: [{ id: 'block-1', type: 'code', sortingKey: 'a0', blockGroup: '1', content: 'print("hello")' }]
34+
blocks: [
35+
{
36+
id: 'block-1',
37+
type: 'code',
38+
sortingKey: 'a0',
39+
blockGroup: '1',
40+
content: 'print("hello")',
41+
metadata: {}
42+
}
43+
]
44+
}
45+
]
46+
}
47+
};
48+
49+
const multiNotebookProject: DeepnoteFile = {
50+
version: '1.0.0',
51+
metadata: { createdAt: '2025-01-01T00:00:00Z' },
52+
project: {
53+
id: 'project-1',
54+
name: 'Multi Notebook Project',
55+
notebooks: [
56+
{
57+
id: 'notebook-1',
58+
name: 'Notebook 1',
59+
blocks: [
60+
{
61+
id: 'block-nb1',
62+
type: 'code',
63+
sortingKey: 'a0',
64+
blockGroup: '1',
65+
content: 'print("nb1-new")',
66+
metadata: {}
67+
}
68+
]
69+
},
70+
{
71+
id: 'notebook-2',
72+
name: 'Notebook 2',
73+
blocks: [
74+
{
75+
id: 'block-nb2',
76+
type: 'code',
77+
sortingKey: 'a0',
78+
blockGroup: '1',
79+
content: 'print("nb2-new")',
80+
metadata: {}
81+
}
82+
]
2583
}
2684
]
2785
}
28-
} as DeepnoteProject;
86+
};
87+
88+
const multiNotebookYaml = serializeDeepnoteFile(multiNotebookProject);
2989

3090
const waitForTimeoutMs = 5000;
3191
const waitForIntervalMs = 50;
@@ -73,6 +133,7 @@ suite('DeepnoteFileChangeWatcher', () => {
73133
mockedNotebookManager = mock<IDeepnoteNotebookManager>();
74134
when(mockedNotebookManager.getOriginalProject(anything())).thenReturn(validProject);
75135
when(mockedNotebookManager.getTheSelectedNotebookForAProject(anything())).thenReturn('notebook-1');
136+
when(mockedNotebookManager.clearNotebookSelection(anything())).thenReturn();
76137
mockNotebookManager = instance(mockedNotebookManager);
77138

78139
// Set up FileSystemWatcher mock
@@ -1200,4 +1261,195 @@ project:
12001261
fbOnDidCreate.dispose();
12011262
});
12021263
});
1264+
1265+
suite('multi-notebook file sync', () => {
1266+
let workspaceSetCaptures: { uriKey: string; cellSourceJoined: string }[] = [];
1267+
let workspaceEditSetStub: sinon.SinonStub | undefined;
1268+
1269+
setup(() => {
1270+
reset(mockedNotebookManager);
1271+
when(mockedNotebookManager.getOriginalProject(anything())).thenReturn(multiNotebookProject);
1272+
when(mockedNotebookManager.getTheSelectedNotebookForAProject(anything())).thenReturn('notebook-1');
1273+
when(mockedNotebookManager.clearNotebookSelection(anything())).thenReturn();
1274+
resetCalls(mockedNotebookManager);
1275+
workspaceSetCaptures = [];
1276+
workspaceEditSetStub = sinon.stub(WorkspaceEdit.prototype, 'set').callsFake((uri: Uri, edits: unknown) => {
1277+
if (!Array.isArray(edits) || edits.length === 0) {
1278+
return;
1279+
}
1280+
1281+
const firstEdit = edits[0] as NotebookEdit;
1282+
if (firstEdit?.newCells && firstEdit.newCells.length > 0) {
1283+
workspaceSetCaptures.push({
1284+
uriKey: uri.toString(),
1285+
cellSourceJoined: firstEdit.newCells.map((c) => c.value).join('\n')
1286+
});
1287+
}
1288+
});
1289+
});
1290+
1291+
teardown(() => {
1292+
workspaceEditSetStub?.restore();
1293+
workspaceEditSetStub = undefined;
1294+
});
1295+
1296+
test('should reload each notebook with its own content when multiple notebooks are open', async () => {
1297+
const basePath = Uri.file('/workspace/multi.deepnote');
1298+
const uriNb1 = basePath.with({ query: 'view=1' });
1299+
const uriNb2 = basePath.with({ query: 'view=2' });
1300+
1301+
const notebook1 = createMockNotebook({
1302+
uri: uriNb1,
1303+
metadata: {
1304+
deepnoteProjectId: 'project-1',
1305+
deepnoteNotebookId: 'notebook-1'
1306+
},
1307+
cells: [
1308+
{
1309+
metadata: { id: 'block-nb1', type: 'code' },
1310+
outputs: [],
1311+
kind: NotebookCellKind.Code,
1312+
document: { getText: () => 'print("nb1-old")', languageId: 'python' }
1313+
}
1314+
]
1315+
});
1316+
1317+
const notebook2 = createMockNotebook({
1318+
uri: uriNb2,
1319+
metadata: {
1320+
deepnoteProjectId: 'project-1',
1321+
deepnoteNotebookId: 'notebook-2'
1322+
},
1323+
cells: [
1324+
{
1325+
metadata: { id: 'block-nb2', type: 'code' },
1326+
outputs: [],
1327+
kind: NotebookCellKind.Code,
1328+
document: { getText: () => 'print("nb2-old")', languageId: 'python' }
1329+
}
1330+
]
1331+
});
1332+
1333+
when(mockedVSCodeNamespaces.workspace.notebookDocuments).thenReturn([notebook1, notebook2]);
1334+
setupMockFs(multiNotebookYaml);
1335+
1336+
onDidChangeFile.fire(basePath);
1337+
1338+
await waitFor(() => applyEditCount >= 2);
1339+
1340+
assert.strictEqual(applyEditCount, 2, 'applyEdit should run once per open notebook');
1341+
assert.strictEqual(workspaceSetCaptures.length, 2, 'each notebook should get a replaceCells edit');
1342+
1343+
const byUri = new Map(workspaceSetCaptures.map((c) => [c.uriKey, c.cellSourceJoined]));
1344+
1345+
assert.include(byUri.get(uriNb1.toString()) ?? '', 'nb1-new');
1346+
assert.notInclude(byUri.get(uriNb1.toString()) ?? '', 'nb2-new');
1347+
assert.include(byUri.get(uriNb2.toString()) ?? '', 'nb2-new');
1348+
assert.notInclude(byUri.get(uriNb2.toString()) ?? '', 'nb1-new');
1349+
});
1350+
1351+
test('should clear notebook selection before processing file change', async () => {
1352+
const basePath = Uri.file('/workspace/multi.deepnote');
1353+
const uriNb1 = basePath.with({ query: 'a=1' });
1354+
const uriNb2 = basePath.with({ query: 'b=2' });
1355+
1356+
const notebook1 = createMockNotebook({
1357+
uri: uriNb1,
1358+
metadata: {
1359+
deepnoteProjectId: 'project-1',
1360+
deepnoteNotebookId: 'notebook-1'
1361+
},
1362+
cells: [
1363+
{
1364+
metadata: { id: 'block-nb1' },
1365+
outputs: [],
1366+
kind: NotebookCellKind.Code,
1367+
document: { getText: () => 'print("nb1-old")' }
1368+
}
1369+
]
1370+
});
1371+
1372+
const notebook2 = createMockNotebook({
1373+
uri: uriNb2,
1374+
metadata: {
1375+
deepnoteProjectId: 'project-1',
1376+
deepnoteNotebookId: 'notebook-2'
1377+
},
1378+
cells: [
1379+
{
1380+
metadata: { id: 'block-nb2' },
1381+
outputs: [],
1382+
kind: NotebookCellKind.Code,
1383+
document: { getText: () => 'print("nb2-old")' }
1384+
}
1385+
]
1386+
});
1387+
1388+
when(mockedVSCodeNamespaces.workspace.notebookDocuments).thenReturn([notebook1, notebook2]);
1389+
setupMockFs(multiNotebookYaml);
1390+
1391+
onDidChangeFile.fire(basePath);
1392+
1393+
await waitFor(() => applyEditCount >= 2);
1394+
1395+
verify(mockedNotebookManager.clearNotebookSelection('project-1')).once();
1396+
});
1397+
1398+
test('should not corrupt other notebooks when one notebook triggers a file change', async () => {
1399+
const basePath = Uri.file('/workspace/multi.deepnote');
1400+
const uriNb1 = basePath.with({ query: 'n=1' });
1401+
const uriNb2 = basePath.with({ query: 'n=2' });
1402+
1403+
const notebook1 = createMockNotebook({
1404+
uri: uriNb1,
1405+
metadata: {
1406+
deepnoteProjectId: 'project-1',
1407+
deepnoteNotebookId: 'notebook-1'
1408+
},
1409+
cells: [
1410+
{
1411+
metadata: { id: 'block-nb1' },
1412+
outputs: [],
1413+
kind: NotebookCellKind.Code,
1414+
document: { getText: () => 'print("nb1-old")' }
1415+
}
1416+
]
1417+
});
1418+
1419+
const notebook2 = createMockNotebook({
1420+
uri: uriNb2,
1421+
metadata: {
1422+
deepnoteProjectId: 'project-1',
1423+
deepnoteNotebookId: 'notebook-2'
1424+
},
1425+
cells: [
1426+
{
1427+
metadata: { id: 'block-nb2' },
1428+
outputs: [],
1429+
kind: NotebookCellKind.Code,
1430+
document: { getText: () => 'print("nb2-old")' }
1431+
}
1432+
]
1433+
});
1434+
1435+
when(mockedVSCodeNamespaces.workspace.notebookDocuments).thenReturn([notebook1, notebook2]);
1436+
setupMockFs(multiNotebookYaml);
1437+
1438+
onDidChangeFile.fire(basePath);
1439+
1440+
await waitFor(() => applyEditCount >= 2);
1441+
1442+
const nb1Cells = workspaceSetCaptures.find((c) => c.uriKey === uriNb1.toString())?.cellSourceJoined;
1443+
const nb2Cells = workspaceSetCaptures.find((c) => c.uriKey === uriNb2.toString())?.cellSourceJoined;
1444+
1445+
assert.isDefined(nb1Cells);
1446+
assert.isDefined(nb2Cells);
1447+
assert.notStrictEqual(nb1Cells, nb2Cells, 'each open notebook must receive distinct deserialized content');
1448+
1449+
assert.include(nb1Cells!, 'nb1-new');
1450+
assert.include(nb2Cells!, 'nb2-new');
1451+
assert.notInclude(nb1Cells!, 'nb2-new', 'notebook-1 must not receive notebook-2 block content');
1452+
assert.notInclude(nb2Cells!, 'nb1-new', 'notebook-2 must not receive notebook-1 block content');
1453+
});
1454+
});
12031455
});

0 commit comments

Comments
 (0)