/ concept-collection / minwebide-demo
Sign in
concept-collection / minwebide-demo
minwebide-demo / scripts / smoke.mjs
108 lines · 5.0 KBBlameHistoryRaw
1// End-to-end smoke test for the project-picker demo:
2// landing → new project → IDE → edit+save → back → rename/duplicate →
3// reopen (persistence + URL routing) → delete → sample project.
4import { chromium } from 'playwright';
5import { spawn } from 'node:child_process';
7const root = new URL('..', import.meta.url).pathname;
8const out = process.argv[2] ?? '.';
9const previewProc = spawn('npx', ['vite', 'preview', '--port', '4179', '--strictPort'], { stdio: 'ignore', cwd: root });
10await new Promise((r) => setTimeout(r, 1500));
12const browser = await chromium.launch({ channel: 'chrome', headless: true });
13const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
14const errors = [];
15page.on('pageerror', (e) => errors.push(e.message));
16page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
17const check = (name, ok) => console.log(`${ok ? 'OK ' : 'FAIL'} ${name}`);
19try {
20 await page.goto('http://localhost:4179/', { waitUntil: 'networkidle' });
21 await page.waitForTimeout(1200);
22 check('landing renders', await page.locator('.landing-empty').count() === 1);
23 await page.screenshot({ path: out + '/p-landing.png' });
25 // create an empty project → should land in the IDE at #/project/<id>
26 await page.getByRole('button', { name: 'New project', exact: true }).click();
27 await page.waitForTimeout(1500);
28 const url1 = page.url();
29 check('URL has project route', /#\/project\/[a-z0-9]+/i.test(url1));
30 check('IDE opened with README', await page.locator('.mw-tab-label', { hasText: 'README.md' }).count() === 1);
31 check('status bar shows project', await page.locator('.mw-statusbar', { hasText: 'untitled' }).count() === 1);
33 // make an edit and save
34 await page.locator('.mw-editor-host').click();
35 await page.keyboard.press('Control+End');
36 await page.keyboard.type('\nPROJECT-MARKER-7\n');
37 await page.keyboard.press('Control+s');
38 await page.waitForTimeout(600);
40 // back to landing via the status bar project item
41 await page.getByTitle('Back to projects').click();
42 await page.waitForTimeout(800);
43 check('back on landing', await page.locator('.landing-project').count() === 1);
45 // rename
46 page.once('dialog', (d) => d.accept('renamed-project'));
47 await page.locator('.landing-project').hover();
48 await page.getByRole('button', { name: 'Rename' }).click();
49 await page.waitForTimeout(300);
50 check('rename applied', await page.locator('.landing-project-name', { hasText: 'renamed-project' }).count() === 1);
52 // duplicate
53 await page.locator('.landing-project').first().hover();
54 await page.getByRole('button', { name: 'Duplicate' }).first().click();
55 await page.waitForTimeout(1200);
56 check('duplicate created', await page.locator('.landing-project').count() === 2);
58 // reopen the duplicate: the saved marker must have been copied
59 await page.locator('.landing-project-name', { hasText: 'renamed-project-copy' }).click();
60 await page.waitForTimeout(1500);
61 const text = (await page.locator('.mw-editor-host').innerText()).replace(/ /g, ' ');
62 check('duplicate carries saved edit', text.includes('PROJECT-MARKER-7'));
63 await page.screenshot({ path: out + '/p-duplicate.png' });
65 // deep-link directly to the original by URL
66 const originalHref = await page.evaluate(() => {
67 const items = JSON.parse(localStorage.getItem('minwebide-demo.projects'));
68 return items.find((p) => p.name === 'renamed-project').id;
69 });
70 await page.goto(`http://localhost:4179/#/project/${originalHref}`, { waitUntil: 'networkidle' });
71 await page.waitForTimeout(1500);
72 check('deep link opens project', await page.locator('.mw-statusbar', { hasText: 'renamed-project' }).count() === 1);
74 // delete both from the landing page
75 await page.goto('http://localhost:4179/#/', { waitUntil: 'networkidle' });
76 await page.waitForTimeout(800);
77 for (let i = 0; i < 2; i++) {
78 page.once('dialog', (d) => d.accept());
79 await page.locator('.landing-project').first().hover();
80 await page.getByRole('button', { name: 'Delete' }).first().click();
81 await page.waitForTimeout(500);
82 }
83 check('projects deleted', await page.locator('.landing-empty').count() === 1);
85 // sample project has the showcase content
86 await page.getByRole('button', { name: 'New sample project' }).click();
87 await page.waitForTimeout(1800);
88 check('sample project opens', await page.locator('.mw-tab-label', { hasText: 'README.md' }).count() === 1);
89 await page.getByText('scripts', { exact: true }).click();
90 await page.waitForTimeout(400);
91 check('sample has runnable script', await page.getByText('sine-wave.js', { exact: true }).count() === 1);
92 await page.screenshot({ path: out + '/p-sample.png' });
94 // unknown project id falls back to landing
95 await page.goto('http://localhost:4179/#/project/nope1234', { waitUntil: 'networkidle' });
96 await page.waitForTimeout(800);
97 check('unknown id falls back to landing', await page.locator('.landing-header').count() === 1);
99 if (errors.length) {
100 console.log('page errors:');
101 for (const e of errors.slice(0, 10)) console.log(' ' + e);
102 } else {
103 console.log('no page errors');
104 }
105} finally {
106 await browser.close();
107 previewProc.kill();
moveopenescclose