feat(ide): add browser VS Code for course notebooks
This commit is contained in:
1 parent
bf3241124c
commit
7d451a7ce9
15 files changed
+430
-50
No files matched your search
@@ -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; });
|
||||
@@ -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
|
||||
Reference in new issue
Block a user