feat(ide): add browser VS Code for course notebooks

This commit is contained in:
ddadmin committed 2026-10-18 22:39:39 +02:00
1 parent bf3241124c
commit 7d451a7ce9
15 files changed
+430 -50

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
// Запуск через browser_ide.sh: настоящий notebook editor, локальное ядро и webview.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { chromium } = require('playwright');
const notebookName = 'lakehouse-browser-smoke.ipynb';
const notebook = JSON.parse(fs.readFileSync(
path.join(__dirname, '../notebooks/01_environment_and_smoke_test.ipynb'), 'utf8',
));
const smokeCell = notebook.cells.findIndex(cell =>
cell.source.join('').includes('smoke_report = run_cluster_smoke(spark)'),
);
async function openNotebook(page) {
await page.keyboard.press('Control+p');
await page.keyboard.type(`/tmp/${notebookName}`);
await page.getByRole('option').filter({ hasText: notebookName }).first().waitFor();
await page.keyboard.press('Enter');
await page.getByRole('list', { name: /Notebook Use/ }).waitFor();
}
async function checkOutput(page) {
// Notebook editor виртуализирует ячейки: сначала показываем нужную ячейку.
const list = page.getByRole('list', { name: /Notebook Use/ });
await list.press('Control+Home');
for (let i = 0; i < smokeCell; i++) await list.press('ArrowDown');
await page.getByRole('button', {
name: `Cell ${smokeCell + 1} of ${notebook.cells.length}`, exact: true,
}).waitFor();
for (let attempt = 0; attempt < 30; attempt++) {
for (const frame of page.frames()) {
const output = frame.getByText('result_sum=499999500000', { exact: false });
if (await output.count() && await output.first().isVisible()) return;
}
await page.waitForTimeout(500);
}
throw new Error('Notebook webview did not render the expected Spark result');
}
(async () => {
assert(smokeCell >= 0, 'Module 1 must contain the cluster smoke cell');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.setDefaultTimeout(30000);
const webviewErrors = [];
page.on('console', message => {
if (/Error loading webview|Could not register.*service.?worker/i.test(message.text())) {
webviewErrors.push(message.text());
}
});
try {
await page.goto('http://localhost:8889', { waitUntil: 'domcontentloaded' });
assert(await page.evaluate(() => isSecureContext && 'serviceWorker' in navigator));
await page.getByRole('tree', { name: 'Files Explorer' }).waitFor();
// Баннер появляется позже Explorer; при повторном тесте доверие уже сохранено.
const restricted = page.getByRole('banner').filter({ hasText: 'Restricted Mode' });
await restricted.waitFor({ timeout: 10000 }).catch(error => {
if (error.name !== 'TimeoutError') throw error;
});
if (await restricted.count()) {
await restricted.getByRole('button', { name: 'Manage', exact: true }).click();
await page.getByRole('button', { name: 'Trust', exact: true }).click();
await page.getByRole('button', { name: 'Close Modal Editor (Escape)' }).click();
await restricted.waitFor({ state: 'hidden' });
}
console.log('PASS: workspace trust enabled');
await openNotebook(page);
await page.getByRole('button', { name: 'Select Kernel', exact: true }).click();
await page.getByRole('option', { name: 'Python Environments...' }).click();
await page.getByRole('option').filter({ hasText: '/usr/bin/python3' }).click();
console.log('PASS: notebook editor and local Python kernel selection');
await page.getByRole('button', { name: 'Run All', exact: true }).click();
await page.getByRole('button', { name: 'Interrupt', exact: true }).waitFor();
await page.getByRole('button', { name: 'Run All', exact: true }).waitFor({ timeout: 120000 });
await checkOutput(page);
await page.keyboard.press('Control+s');
console.log('PASS: Spark output rendered in notebook webview over HTTP localhost');
await page.getByRole('tab', { name: notebookName, exact: true })
.getByRole('button', { name: 'Close (Ctrl+W)' }).click();
await openNotebook(page);
await checkOutput(page);
assert.deepEqual(webviewErrors, []);
await page.screenshot({ path: '/artifacts/notebook.png' });
await page.getByRole('tab', { name: notebookName, exact: true })
.getByRole('button', { name: 'Close (Ctrl+W)' }).click();
console.log('PASS: saved notebook reopened with its output');
} catch (error) {
await page.screenshot({ path: '/artifacts/failure.png' }).catch(() => {});
throw error;
} finally {
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Проверка UI на Linux Docker Engine; зависимости браузера остаются в контейнере.
set -euo pipefail
cd "$(dirname "$0")/.."
artifact_dir=$(mktemp -d /tmp/lakehouse-browser-check.XXXXXX)
echo "Browser artifacts: $artifact_dir"
docker compose exec -T ide cp /opt/work/01_environment_and_smoke_test.ipynb \
/tmp/lakehouse-browser-smoke.ipynb
docker run --rm --network host --shm-size=1g \
-v "$PWD:/repo:ro" -v "$artifact_dir:/artifacts" \
mcr.microsoft.com/playwright:v1.56.1-noble \
bash -c 'npm install --prefix /tmp/browser-test --no-audit --no-fund playwright@1.56.1 >/dev/null &&
NODE_PATH=/tmp/browser-test/node_modules node /repo/tests/browser_ide.cjs'
docker compose exec -T ide python3 - <<'PY'
import json
from pathlib import Path
path = Path('/tmp/lakehouse-browser-smoke.ipynb')
notebook = json.loads(path.read_text())
cells = [cell for cell in notebook['cells'] if cell['cell_type'] == 'code']
assert all(cell['execution_count'] is not None for cell in cells)
assert not any(output['output_type'] == 'error'
for cell in cells for output in cell.get('outputs', []))
output = '\n'.join(''.join(item.get('text', []))
for cell in cells for item in cell.get('outputs', []))
assert 'master_url=spark://spark-master:7077' in output
assert 'result_sum=499999500000' in output
path.unlink()
print(f'PASS: all {len(cells)} code cells executed and saved without errors')
PY