-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtest-export-workbook.mjs
More file actions
328 lines (294 loc) · 17.1 KB
/
Copy pathtest-export-workbook.mjs
File metadata and controls
328 lines (294 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/**
* Playwright test: Export Workbooks & Packages modal
* Tests modal open/close, section visibility toggling, and export dispatch.
*/
import { chromium } from 'playwright';
const BASE = 'http://localhost:8085';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
const logs = [];
page.on('console', msg => logs.push(`[${msg.type()}] ${msg.text()}`));
console.log('1. Loading SciREPL...');
await page.goto(BASE, { waitUntil: 'networkidle', timeout: 30000 });
// Wait for app to initialize
await page.waitForFunction(() => window.notebookManager && window.fileIO, { timeout: 15000 });
console.log(' App initialized.');
// 2. Open menu and click Export Workbooks & Packages
console.log('2. Opening Export Workbooks & Packages modal...');
await page.click('#menu-btn');
await page.waitForSelector('#menu-modal:not(.hidden)', { timeout: 5000 });
await page.click('#btn-export-workbook');
await page.waitForSelector('#export-workbook-modal:not(.hidden)', { timeout: 5000 });
console.log(' Modal opened.');
// 3. Verify default state: srwb selected, scope visible, kernel/archive hidden
console.log('3. Verifying default modal state...');
const defaultState = await page.evaluate(() => {
const modal = document.getElementById('export-workbook-modal');
const format = modal.querySelector('input[name="wb-export-format"]:checked').value;
const scope = modal.querySelector('input[name="wb-export-scope"]:checked').value;
const scopeHidden = document.getElementById('wb-scope-section').classList.contains('hidden');
const kernelHidden = document.getElementById('wb-kernel-section').classList.contains('hidden');
const archiveHidden = document.getElementById('wb-archive-section').classList.contains('hidden');
return { format, scope, scopeHidden, kernelHidden, archiveHidden };
});
console.log(` Format: ${defaultState.format}, Scope: ${defaultState.scope}`);
console.log(` Scope hidden: ${defaultState.scopeHidden}, Kernel hidden: ${defaultState.kernelHidden}, Archive hidden: ${defaultState.archiveHidden}`);
if (defaultState.format !== 'srwb') throw new Error('Default format should be srwb');
if (defaultState.scope !== 'current') throw new Error('Default scope should be current');
if (defaultState.scopeHidden) throw new Error('Scope section should be visible for srwb');
if (!defaultState.kernelHidden) throw new Error('Kernel section should be hidden for srwb');
if (!defaultState.archiveHidden) throw new Error('Archive section should be hidden for srwb');
// 4. Switch to ipynb — kernel should show, archive should stay hidden
console.log('4. Switching to ipynb format...');
await page.click('input[name="wb-export-format"][value="ipynb"]');
const ipynbState = await page.evaluate(() => {
const scopeHidden = document.getElementById('wb-scope-section').classList.contains('hidden');
const kernelHidden = document.getElementById('wb-kernel-section').classList.contains('hidden');
const archiveHidden = document.getElementById('wb-archive-section').classList.contains('hidden');
return { scopeHidden, kernelHidden, archiveHidden };
});
console.log(` Scope hidden: ${ipynbState.scopeHidden}, Kernel hidden: ${ipynbState.kernelHidden}, Archive hidden: ${ipynbState.archiveHidden}`);
if (ipynbState.scopeHidden) throw new Error('Scope should be visible for ipynb');
if (ipynbState.kernelHidden) throw new Error('Kernel should be visible for ipynb');
if (!ipynbState.archiveHidden) throw new Error('Archive should be hidden for ipynb + current tab');
// 4b. Switch scope to "all" — archive should now show for ipynb
console.log('4b. Switching ipynb scope to all tabs...');
await page.click('input[name="wb-export-scope"][value="all"]');
const ipynbAllState = await page.evaluate(() => {
return document.getElementById('wb-archive-section').classList.contains('hidden');
});
console.log(` Archive hidden: ${ipynbAllState}`);
if (ipynbAllState) throw new Error('Archive should be visible for ipynb + all tabs');
// Reset scope back to current for next tests
await page.click('input[name="wb-export-scope"][value="current"]');
// 5. Switch to package — scope should hide, archive should show, kernel should hide
console.log('5. Switching to package format...');
await page.click('input[name="wb-export-format"][value="package"]');
const pkgState = await page.evaluate(() => {
const scopeHidden = document.getElementById('wb-scope-section').classList.contains('hidden');
const kernelHidden = document.getElementById('wb-kernel-section').classList.contains('hidden');
const archiveHidden = document.getElementById('wb-archive-section').classList.contains('hidden');
return { scopeHidden, kernelHidden, archiveHidden };
});
console.log(` Scope hidden: ${pkgState.scopeHidden}, Kernel hidden: ${pkgState.kernelHidden}, Archive hidden: ${pkgState.archiveHidden}`);
if (!pkgState.scopeHidden) throw new Error('Scope should be hidden for package');
if (!pkgState.kernelHidden) throw new Error('Kernel should be hidden for package');
if (pkgState.archiveHidden) throw new Error('Archive should be visible for package');
// 6. Close modal via backdrop click
console.log('6. Testing modal close (backdrop click)...');
await page.click('#export-workbook-modal', { position: { x: 5, y: 5 } });
const closed = await page.evaluate(() =>
document.getElementById('export-workbook-modal').classList.contains('hidden')
);
if (!closed) throw new Error('Modal should close on backdrop click');
console.log(' Modal closed.');
// 7. Re-open and close via X button
console.log('7. Testing modal close (X button)...');
await page.click('#menu-btn');
await page.waitForSelector('#menu-modal:not(.hidden)', { timeout: 5000 });
await page.click('#btn-export-workbook');
await page.waitForSelector('#export-workbook-modal:not(.hidden)', { timeout: 5000 });
await page.click('#export-workbook-modal .modal-close');
const closed2 = await page.evaluate(() =>
document.getElementById('export-workbook-modal').classList.contains('hidden')
);
if (!closed2) throw new Error('Modal should close on X click');
console.log(' Modal closed via X.');
// 8. Test .srwb export (current tab) — intercept download
console.log('8. Testing .srwb export (current tab)...');
await page.click('#menu-btn');
await page.waitForSelector('#menu-modal:not(.hidden)', { timeout: 5000 });
await page.click('#btn-export-workbook');
await page.waitForSelector('#export-workbook-modal:not(.hidden)', { timeout: 5000 });
// Make sure srwb is selected
await page.click('input[name="wb-export-format"][value="srwb"]');
await page.click('input[name="wb-export-scope"][value="current"]');
// Intercept download
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 10000 }),
page.click('#btn-do-export-workbook')
]);
const filename = download.suggestedFilename();
console.log(` Downloaded: ${filename}`);
if (!filename.endsWith('.srwb')) throw new Error(`Expected .srwb file, got: ${filename}`);
// Read and validate content
const downloadPath = await download.path();
const fs = await import('fs');
const content = fs.readFileSync(downloadPath, 'utf-8');
const srwb = JSON.parse(content);
console.log(` Format: ${srwb.format}, Version: ${srwb.format_version}`);
console.log(` Notebook name: ${srwb.notebook?.name || 'N/A'}`);
if (srwb.format !== 'srwb') throw new Error('Wrong format');
if (srwb.format_version !== '1.0') throw new Error('Wrong format_version');
if (!srwb.notebook) throw new Error('Missing notebook field');
if (!srwb.exported_at) throw new Error('Missing exported_at');
// 9. Test .srwb export (all tabs)
console.log('9. Testing .srwb export (all tabs)...');
await page.click('#menu-btn');
await page.waitForSelector('#menu-modal:not(.hidden)', { timeout: 5000 });
await page.click('#btn-export-workbook');
await page.waitForSelector('#export-workbook-modal:not(.hidden)', { timeout: 5000 });
await page.click('input[name="wb-export-format"][value="srwb"]');
await page.click('input[name="wb-export-scope"][value="all"]');
const [download2] = await Promise.all([
page.waitForEvent('download', { timeout: 10000 }),
page.click('#btn-do-export-workbook')
]);
const filename2 = download2.suggestedFilename();
console.log(` Downloaded: ${filename2}`);
if (!filename2.endsWith('.srwb')) throw new Error(`Expected .srwb, got: ${filename2}`);
const content2 = fs.readFileSync(await download2.path(), 'utf-8');
const srwb2 = JSON.parse(content2);
console.log(` Format: ${srwb2.format}, workbook notebooks: ${srwb2.workbook?.notebooks?.length || 0}`);
if (!srwb2.workbook) throw new Error('Missing workbook field for all-tabs export');
if (!srwb2.workbook.notebooks || srwb2.workbook.notebooks.length === 0) throw new Error('Empty notebooks array');
// 10. Add a code cell so ipynb has content to export
console.log('10. Adding a code cell for .ipynb test...');
await page.evaluate(() => {
// Add a cell via the global API
if (window._cells && window._cells.length === 0) {
window._cells.push({
id: 1,
type: 'code',
language: 'python',
code: 'print("hello")',
outputs: []
});
window._cellCounter = 1;
}
});
// 11. Test .ipynb export
console.log('11. Testing .ipynb export...');
await page.click('#menu-btn');
await page.waitForSelector('#menu-modal:not(.hidden)', { timeout: 5000 });
await page.click('#btn-export-workbook');
await page.waitForSelector('#export-workbook-modal:not(.hidden)', { timeout: 5000 });
await page.click('input[name="wb-export-format"][value="ipynb"]');
await page.click('input[name="wb-export-scope"][value="current"]');
const [download3] = await Promise.all([
page.waitForEvent('download', { timeout: 10000 }),
page.click('#btn-do-export-workbook')
]);
const filename3 = download3.suggestedFilename();
console.log(` Downloaded: ${filename3}`);
if (!filename3.endsWith('.ipynb')) throw new Error(`Expected .ipynb, got: ${filename3}`);
const content3 = fs.readFileSync(await download3.path(), 'utf-8');
const ipynb = JSON.parse(content3);
console.log(` nbformat: ${ipynb.nbformat}, cells: ${ipynb.cells?.length || 0}`);
if (ipynb.nbformat !== 4) throw new Error('Wrong nbformat');
if (!ipynb.metadata?.kernelspec) throw new Error('Missing kernelspec');
// 12. Create a second notebook and test .ipynb all-tabs zip export
console.log('12. Creating second notebook for all-tabs .ipynb test...');
await page.evaluate(() => {
const nm = window.notebookManager;
const nb2 = nm.createNotebook({ name: 'Test Notebook 2' });
// Add a cell to the second notebook
nb2.cells = [{
id: 1, type: 'code', language: 'r',
code: 'print("hello from R")', outputs: []
}];
nb2.cellCounter = 1;
// Switch back to first notebook so nb2 is inactive
const first = nm.getNotebooks()[0];
nm.switchTo(first.id);
});
console.log('13. Testing .ipynb export (all tabs → zip)...');
await page.click('#menu-btn');
await page.waitForSelector('#menu-modal:not(.hidden)', { timeout: 5000 });
await page.click('#btn-export-workbook');
await page.waitForSelector('#export-workbook-modal:not(.hidden)', { timeout: 5000 });
await page.click('input[name="wb-export-format"][value="ipynb"]');
await page.click('input[name="wb-export-scope"][value="all"]');
const [download4] = await Promise.all([
page.waitForEvent('download', { timeout: 10000 }),
page.click('#btn-do-export-workbook')
]);
const filename4 = download4.suggestedFilename();
console.log(` Downloaded: ${filename4}`);
if (!filename4.endsWith('.zip')) throw new Error(`Expected .zip for multi-tab ipynb, got: ${filename4}`);
console.log(' Multi-tab .ipynb zip export works.');
// 14. Test package format shows file tree
console.log('14. Testing package file tree...');
await page.click('#menu-btn');
await page.waitForSelector('#menu-modal:not(.hidden)', { timeout: 5000 });
await page.click('#btn-export-workbook');
await page.waitForSelector('#export-workbook-modal:not(.hidden)', { timeout: 5000 });
await page.click('input[name="wb-export-format"][value="package"]');
// Wait a tick for tree to populate
await page.waitForTimeout(200);
const treeState = await page.evaluate(() => {
const section = document.getElementById('wb-filetree-section');
const tree = document.getElementById('wb-filetree');
const hidden = section.classList.contains('hidden');
const checkboxes = tree.querySelectorAll('input[type="checkbox"][data-path]');
const folders = tree.querySelectorAll('input[type="checkbox"][data-dir]');
return {
hidden,
fileCount: checkboxes.length,
folderCount: folders.length,
allChecked: Array.from(checkboxes).every(cb => cb.checked)
};
});
console.log(` Tree visible: ${!treeState.hidden}, Files: ${treeState.fileCount}, Folders: ${treeState.folderCount}, All checked: ${treeState.allChecked}`);
if (treeState.hidden) throw new Error('File tree should be visible for package format');
if (treeState.fileCount === 0) throw new Error('File tree should have at least one file');
if (!treeState.allChecked) throw new Error('All files should be checked by default');
// 15. Test folder checkbox toggles children
console.log('15. Testing folder checkbox toggle...');
const toggleResult = await page.evaluate(() => {
const tree = document.getElementById('wb-filetree');
const firstFolder = tree.querySelector('input[type="checkbox"][data-dir]');
if (!firstFolder) return { error: 'no folder found' };
const dir = firstFolder.dataset.dir;
// Uncheck folder
firstFolder.checked = false;
firstFolder.dispatchEvent(new Event('change', { bubbles: true }));
const children = tree.querySelectorAll(`input[type="checkbox"][data-path]`);
// Find children that belong to this folder
const dirChildren = Array.from(children).filter(cb => cb.dataset.path.startsWith(dir === '.' ? '' : dir + '/') || dir === '.');
const allUnchecked = dirChildren.every(cb => !cb.checked);
// Re-check folder
firstFolder.checked = true;
firstFolder.dispatchEvent(new Event('change', { bubbles: true }));
const allRechecked = dirChildren.every(cb => cb.checked);
return { allUnchecked, allRechecked };
});
if (toggleResult.error) {
console.log(` Skipping toggle test: ${toggleResult.error}`);
} else {
console.log(` Uncheck folder → children unchecked: ${toggleResult.allUnchecked}, Recheck → children checked: ${toggleResult.allRechecked}`);
if (!toggleResult.allUnchecked) throw new Error('Unchecking folder should uncheck all children');
if (!toggleResult.allRechecked) throw new Error('Checking folder should check all children');
}
// 16. Test file tree hidden when switching away from package
console.log('16. Testing file tree hides for non-package formats...');
await page.click('input[name="wb-export-format"][value="srwb"]');
const treeHidden = await page.evaluate(() =>
document.getElementById('wb-filetree-section').classList.contains('hidden')
);
if (!treeHidden) throw new Error('File tree should be hidden for srwb format');
console.log(' File tree hidden for srwb.');
// Close modal
await page.click('#export-workbook-modal .modal-close');
// 17. Verify menu still has Export Document button
console.log('17. Verifying Export Document button still exists...');
await page.click('#menu-btn');
await page.waitForSelector('#menu-modal:not(.hidden)', { timeout: 5000 });
const hasExportDoc = await page.evaluate(() => !!document.getElementById('btn-export'));
if (!hasExportDoc) throw new Error('Export Document button missing from menu');
console.log(' Export Document button present.');
// Verify old buttons are gone
const hasOldIpynb = await page.evaluate(() => !!document.getElementById('btn-export-ipynb'));
const hasOldPkg = await page.evaluate(() => !!document.getElementById('btn-export-package'));
if (hasOldIpynb) throw new Error('Old btn-export-ipynb should be removed');
if (hasOldPkg) throw new Error('Old btn-export-package should be removed');
console.log(' Old buttons removed correctly.');
console.log('\n✓ All export workbook tests passed!');
await browser.close();
process.exit(0);
})().catch(err => {
console.error('\n✗ Test failed:', err.message);
process.exit(1);
});