Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

# Test / coverage
/coverage/
/test-results/
/playwright-report/

# Dependencies (reuse a sibling install or `npm ci`)
# No trailing slash so a `node_modules` symlink is ignored too.
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,22 @@ and the bootstrap — is held at **100/100/100/100** (statements / branches /
functions / lines). The fetch, crypto, and storage seams are injected, so the
suite needs no mocking libraries.

### End-to-end (real browser)

happy-dom has no real layout or scrollbars, so render-layer bugs (e.g. the
editor highlight drifting behind the selection when a scrollbar shrinks the
textarea's client box) can't be caught by the unit suite. A small Playwright
harness mounts the real `src/` modules in Chromium for those cases.

```bash
npx playwright install chromium # once per machine
npm run test:e2e
```

The harness (`tests/e2e/`) serves the repo over HTTP and imports the actual
source as native ESM — no bundling, always current. It is **not** part of
`npm test` or the coverage gate.

## License

Apache-2.0.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
"build": "node build/build.mjs",
"test": "vitest run --coverage --config tests/vitest.config.ts",
"test:watch": "vitest --config tests/vitest.config.ts",
"test:e2e": "playwright test",
"dev": "node build/build.mjs && python3 -m http.server -d dist 8900"
},
"devDependencies": {
"@playwright/test": "^1.48.0",
"@vitest/coverage-v8": "^2.1.8",
"esbuild": "^0.21.5",
"happy-dom": "^15.11.0",
Expand Down
25 changes: 25 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig } from '@playwright/test';

// Real-browser regression tests. happy-dom (the unit layer) has no scrollbar or
// real box layout, so render-layer bugs — e.g. the editor's highlight drifting
// behind the selection when a scrollbar shrinks the textarea's client box —
// can only be caught in a real engine. These run separately from `npm test`.
//
// Setup once per machine: `npx playwright install chromium`.
// Run: `npm run test:e2e`.
export default defineConfig({
testDir: './tests/e2e',
testMatch: '**/*.spec.js',
// Serve the repo root over HTTP so the harness can import the *actual* source
// modules (/src/**) as native ESM — no bundling, always current.
webServer: {
command: 'python3 -m http.server -d . 5599',
url: 'http://127.0.0.1:5599/tests/e2e/editor.html',
reuseExistingServer: !process.env.CI,
timeout: 30_000,
},
use: {
baseURL: 'http://127.0.0.1:5599',
browserName: 'chromium',
},
});
44 changes: 44 additions & 0 deletions tests/e2e/editor-alignment.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { test, expect } from '@playwright/test';

// Regression guard for the editor's highlight/selection alignment.
//
// The bug: a long line gives the <textarea> a horizontal scrollbar, which
// shrinks its clientHeight (~10px). That made the textarea's max vertical
// scrollTop larger than the highlight <pre>'s (overflow:hidden, no scrollbar),
// so scrolling to the bottom clamped pre.scrollTop below ta.scrollTop and the
// painted glyphs lagged the native selection — worst on the last line.
// Repro shape: shift-click a CREATE TABLE, scroll down, select the last line.
//
// This can only be caught in a real engine — happy-dom has no scrollbar layout.
test.describe('editor highlight/selection alignment', () => {
test('highlight tracks the textarea scrolled to the bottom with a long line', async ({ page }) => {
await page.goto('/tests/e2e/editor.html');
await page.waitForFunction(() => window.__ready === true);

// Tall content whose LAST line is long enough to force a horizontal scrollbar.
const sql = [
...Array.from({ length: 30 }, (_, i) => `SELECT col_${i} FROM t WHERE x = ${i}`),
`COMMENT '${'x'.repeat(400)}'`,
].join('\n');
await page.evaluate((s) => window.__setSql(s), sql);

const m = await page.evaluate(() => {
const ta = window.__app.dom.editorTextarea;
const pre = window.__app.dom.editorPre;
ta.focus();
ta.scrollTop = ta.scrollHeight; // scroll to the very bottom
ta.dispatchEvent(new Event('scroll'));
return {
taClientH: ta.clientHeight,
preClientH: pre.clientHeight,
taScrollTop: ta.scrollTop,
preScrollTop: pre.scrollTop,
};
});

// The textarea must not reserve scrollbar space the highlight lacks…
expect(m.taClientH).toBe(m.preClientH);
// …so the highlight reaches the same offset instead of clamping behind it.
expect(Math.abs(m.taScrollTop - m.preScrollTop)).toBeLessThan(1);
});
});
36 changes: 36 additions & 0 deletions tests/e2e/editor.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>editor harness</title>
<!-- the real stylesheet, so .sql-textarea / .sql-pre lay out exactly as shipped -->
<link rel="stylesheet" href="/src/styles.css" />
<style>
html, body { margin: 0; background: #111; }
/* .sql-editor is height/width:100%, so the mount host needs a fixed box. */
#host { width: 680px; height: 340px; }
</style>
</head>
<body data-theme="dark">
<div id="host"></div>
<!-- Mount the real editor module with a minimal stub controller. No bundling:
the browser resolves the /src ESM graph directly. -->
<script type="module">
import { mountEditor } from '/src/ui/editor.js';
import { createState } from '/src/state.js';

const state = createState({ loadStr: (k, d) => d, loadJSON: (k, d) => d });
const app = { state, dom: {}, actions: { rerenderTabs() {}, updateSaveBtn() {} } };
window.__app = app;
mountEditor(app, document.getElementById('host'));

// Set editor content the way typing/replace does (drives paint()).
window.__setSql = (sql) => {
const ta = app.dom.editorTextarea;
ta.value = sql;
ta.dispatchEvent(new Event('input', { bubbles: true }));
};
window.__ready = true;
</script>
</body>
</html>
Loading