2026-04-29 11:12:45 -07:00
|
|
|
/**
|
2026-05-15 20:31:27 -07:00
|
|
|
* test_wysiwyg.js — Styled-source WYSIWYG integration tests.
|
2026-04-29 11:12:45 -07:00
|
|
|
*
|
2026-05-15 20:31:27 -07:00
|
|
|
* Tests the new styled-source editor implementation. Key differences
|
|
|
|
|
* from the old test suite:
|
2026-04-29 11:12:45 -07:00
|
|
|
*
|
2026-05-15 20:31:27 -07:00
|
|
|
* - No data-speculative, no <strong>/<em>/<del> DOM elements.
|
|
|
|
|
* The editor always stores raw markdown; CSS renders it visually.
|
|
|
|
|
* - Inline formatting uses .md-bold, .md-italic, .md-code spans
|
|
|
|
|
* with .md-delim children holding the delimiter characters.
|
|
|
|
|
* - getMarkdown() reads textContent directly — always returns the
|
|
|
|
|
* original markdown source, never converted HTML.
|
|
|
|
|
* - Block structure uses <div class="md-*"> elements, not <p>/<h1> etc.
|
|
|
|
|
*
|
|
|
|
|
* Run headless: node test/integration/test_wysiwyg.js
|
|
|
|
|
* Run against dev server: node test/integration/test_wysiwyg.js --port=5023
|
2026-04-29 11:12:45 -07:00
|
|
|
*/
|
2026-05-15 20:31:27 -07:00
|
|
|
|
|
|
|
|
const { chromium } = require('playwright');
|
2026-04-29 11:12:45 -07:00
|
|
|
const { createServer } = require('./server');
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const HEADLESS = !process.argv.includes('--headed');
|
|
|
|
|
const PORT = (() => {
|
|
|
|
|
const portArg = process.argv.find(arg => arg.startsWith('--port='));
|
|
|
|
|
return portArg ? parseInt(portArg.split('=')[1]) : 5023;
|
|
|
|
|
})();
|
|
|
|
|
const USE_DEV_SERVER = process.argv.includes('--port');
|
|
|
|
|
const DELAY = 20; // ms between keystrokes
|
|
|
|
|
|
|
|
|
|
// ── State ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
let browser, page, server;
|
|
|
|
|
let passed = 0, failed = 0;
|
|
|
|
|
const errors = [];
|
|
|
|
|
|
|
|
|
|
// ── Setup / teardown ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function serverStart() {
|
|
|
|
|
var liveServer = require("live-server");
|
|
|
|
|
var params = {
|
|
|
|
|
port: PORT,
|
|
|
|
|
host: "0.0.0.0",
|
|
|
|
|
open: true,
|
|
|
|
|
root: "test/integration",
|
|
|
|
|
mount: [
|
|
|
|
|
['/static', 'dist/ribbit'],
|
|
|
|
|
['/test', 'test/integration'],
|
|
|
|
|
],
|
|
|
|
|
logLevel: 2, // 0 = errors only, 1 = some, 2 = lots
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log(`\n🐸 Ribbit dev server running on http://localhost:${params['port']}`);
|
|
|
|
|
liveServer.start(params);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 11:12:45 -07:00
|
|
|
|
|
|
|
|
async function setup() {
|
2026-05-15 20:31:27 -07:00
|
|
|
|
|
|
|
|
if (!USE_DEV_SERVER) {
|
|
|
|
|
await serverStart();
|
|
|
|
|
}
|
|
|
|
|
browser = await chromium.launch({ headless: HEADLESS });
|
|
|
|
|
page = await browser.newPage();
|
|
|
|
|
await page.goto(`http://localhost:${PORT}`);
|
|
|
|
|
await page.waitForFunction(() => window.__ribbitReady === true, { timeout: 10000 });
|
2026-04-29 11:12:45 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function teardown() {
|
2026-05-15 20:31:27 -07:00
|
|
|
//if (browser) { await browser.close(); }
|
|
|
|
|
//if (server) { await server.stop(); }
|
2026-04-29 11:12:45 -07:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Editor helpers ────────────────────────────────────────────────────────────
|
2026-04-29 11:12:45 -07:00
|
|
|
|
|
|
|
|
/**
|
2026-05-15 20:31:27 -07:00
|
|
|
* Reset the editor to an empty state in wysiwyg mode.
|
|
|
|
|
* Clears the DOM and places the cursor ready for typing.
|
2026-04-29 11:12:45 -07:00
|
|
|
*/
|
2026-05-15 20:31:27 -07:00
|
|
|
async function resetEditor() {
|
|
|
|
|
await page.evaluate(() => {
|
|
|
|
|
const editor = window.__ribbitEditor;
|
|
|
|
|
editor.wysiwyg();
|
|
|
|
|
editor.element.innerHTML = '';
|
|
|
|
|
});
|
|
|
|
|
await page.focus('#ribbit');
|
|
|
|
|
await page.waitForTimeout(30);
|
2026-04-29 11:12:45 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Type a string one character at a time with delay between each.
|
2026-05-15 20:31:27 -07:00
|
|
|
* Matches real user behaviour so block/inline transforms fire correctly.
|
2026-04-29 11:12:45 -07:00
|
|
|
*/
|
|
|
|
|
async function typeString(text) {
|
|
|
|
|
for (const character of text) {
|
2026-05-15 20:31:27 -07:00
|
|
|
await page.keyboard.type(character);
|
|
|
|
|
await page.waitForTimeout(DELAY);
|
2026-04-29 11:12:45 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
/**
|
|
|
|
|
* Press a special key (Enter, Backspace, ArrowRight, etc).
|
|
|
|
|
*/
|
|
|
|
|
async function pressKey(key) {
|
|
|
|
|
await page.keyboard.press(key);
|
|
|
|
|
await page.waitForTimeout(DELAY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the editor's current innerHTML.
|
|
|
|
|
*/
|
2026-04-29 11:12:45 -07:00
|
|
|
async function getHTML() {
|
2026-05-15 20:31:27 -07:00
|
|
|
return page.evaluate(() => document.getElementById('ribbit').innerHTML);
|
2026-04-29 11:12:45 -07:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
/**
|
|
|
|
|
* Get the editor's current markdown via getMarkdown().
|
|
|
|
|
*/
|
2026-04-29 11:12:45 -07:00
|
|
|
async function getMarkdown() {
|
2026-05-15 20:31:27 -07:00
|
|
|
return page.evaluate(() => window.__ribbitEditor.getMarkdown());
|
2026-04-29 11:12:45 -07:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
/**
|
|
|
|
|
* Get all CSS classes on block divs inside the editor.
|
|
|
|
|
*/
|
|
|
|
|
async function getBlockClasses() {
|
|
|
|
|
return page.evaluate(() =>
|
|
|
|
|
Array.from(document.getElementById('ribbit').children)
|
|
|
|
|
.map(block => block.className)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Test runner ───────────────────────────────────────────────────────────────
|
2026-04-29 11:12:45 -07:00
|
|
|
|
|
|
|
|
function assert(condition, message) {
|
|
|
|
|
if (!condition) { throw new Error(message); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function test(name, fn) {
|
|
|
|
|
try {
|
|
|
|
|
await fn();
|
|
|
|
|
passed++;
|
|
|
|
|
console.log(` ✓ ${name}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
failed++;
|
2026-05-15 20:31:27 -07:00
|
|
|
errors.push({ name, message: error.message });
|
2026-04-29 11:12:45 -07:00
|
|
|
console.log(` ✗ ${name}`);
|
|
|
|
|
console.log(` ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-29 11:12:45 -07:00
|
|
|
async function runTests() {
|
2026-05-15 20:31:27 -07:00
|
|
|
console.log('\nStyled-source WYSIWYG Integration Tests\n');
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Block classification ───────────────────────────────────────────────────
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
console.log('Block classification:');
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('plain text becomes md-paragraph', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString("hello\n\n");
|
|
|
|
|
const classes = await getBlockClasses();
|
|
|
|
|
assert(classes.some(c => c.includes('md-paragraph')), `Expected md-paragraph, got: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
/*
|
|
|
|
|
await test('# space becomes md-h1', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('#');
|
|
|
|
|
let classes = await getBlockClasses();
|
|
|
|
|
assert(!classes.some(c => c.includes('md-h')), `Premature heading after just #: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString(' ');
|
|
|
|
|
classes = await getBlockClasses();
|
|
|
|
|
assert(classes.some(c => c.includes('md-h1')), `Expected md-h1 after "# ", got: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('Title');
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown.includes('# Title'), `Expected "# Title" in markdown: ${markdown}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('## space becomes md-h2', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('## ');
|
|
|
|
|
const classes = await getBlockClasses();
|
|
|
|
|
assert(classes.some(c => c.includes('md-h2')), `Expected md-h2, got: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('### space becomes md-h3', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('### ');
|
|
|
|
|
const classes = await getBlockClasses();
|
|
|
|
|
assert(classes.some(c => c.includes('md-h3')), `Expected md-h3, got: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('> space becomes md-blockquote', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('>');
|
|
|
|
|
let classes = await getBlockClasses();
|
|
|
|
|
assert(!classes.some(c => c.includes('md-blockquote')), `Premature blockquote: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString(' ');
|
|
|
|
|
classes = await getBlockClasses();
|
|
|
|
|
assert(classes.some(c => c.includes('md-blockquote')), `Expected md-blockquote, got: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('- space becomes md-list-item', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('-');
|
|
|
|
|
let classes = await getBlockClasses();
|
|
|
|
|
assert(!classes.some(c => c.includes('md-list')), `Premature list: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString(' ');
|
|
|
|
|
classes = await getBlockClasses();
|
|
|
|
|
assert(classes.some(c => c.includes('md-list-item')), `Expected md-list-item, got: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('1. space becomes md-ol-list-item', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('1. ');
|
|
|
|
|
const classes = await getBlockClasses();
|
|
|
|
|
assert(classes.some(c => c.includes('md-ol-list-item')), `Expected md-ol-list-item, got: ${classes}`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Inline formatting ──────────────────────────────────────────────────────
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
console.log('\nInline formatting:');
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('**bold** produces md-bold span', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
|
|
|
|
await typeString('**bold**');
|
2026-05-15 20:31:27 -07:00
|
|
|
const html = await getHTML();
|
|
|
|
|
assert(html.includes('md-bold'), `Expected md-bold span: ${html}`);
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown === '**bold**', `Expected "**bold**", got: "${markdown}"`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('*italic* produces md-italic span', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('*italic*');
|
|
|
|
|
const html = await getHTML();
|
|
|
|
|
assert(html.includes('md-italic'), `Expected md-italic span: ${html}`);
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown === '*italic*', `Expected "*italic*", got: "${markdown}"`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('***bold-italic*** produces md-bold-italic span', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('***both***');
|
|
|
|
|
const html = await getHTML();
|
|
|
|
|
assert(html.includes('md-bold-italic'), `Expected md-bold-italic span: ${html}`);
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown === '***both***', `Expected "***both***", got: "${markdown}"`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('`code` produces md-code span', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('`code`');
|
|
|
|
|
const html = await getHTML();
|
|
|
|
|
assert(html.includes('md-code'), `Expected md-code span: ${html}`);
|
2026-04-30 00:25:30 -07:00
|
|
|
const markdown = await getMarkdown();
|
2026-05-15 20:31:27 -07:00
|
|
|
assert(markdown === '`code`', `Expected "\`code\`", got: "${markdown}"`);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('~~strike~~ produces md-strikethrough span', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('~~gone~~');
|
|
|
|
|
const html = await getHTML();
|
|
|
|
|
assert(html.includes('md-strikethrough'), `Expected md-strikethrough span: ${html}`);
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown === '~~gone~~', `Expected "~~gone~~", got: "${markdown}"`);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('delimiters are present in DOM as md-delim spans', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('**bold**');
|
2026-04-30 00:25:30 -07:00
|
|
|
const html = await getHTML();
|
2026-05-15 20:31:27 -07:00
|
|
|
assert(html.includes('md-delim'), `Expected md-delim spans: ${html}`);
|
|
|
|
|
// The delimiter text ** must appear in the DOM
|
|
|
|
|
assert(html.includes('**'), `Delimiter text missing from DOM: ${html}`);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('mixed inline on one line round-trips correctly', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('hello **world** and *italic*');
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(
|
|
|
|
|
markdown === 'hello **world** and *italic*',
|
|
|
|
|
`Round-trip failed: "${markdown}"`
|
|
|
|
|
);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── getMarkdown round-trips ────────────────────────────────────────────────
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
console.log('\ngetMarkdown round-trips:');
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('heading round-trips', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('# Hello World');
|
2026-04-29 11:12:45 -07:00
|
|
|
const markdown = await getMarkdown();
|
2026-05-15 20:31:27 -07:00
|
|
|
assert(markdown === '# Hello World', `Expected "# Hello World", got: "${markdown}"`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('blockquote round-trips', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('> quoted text');
|
2026-04-29 11:12:45 -07:00
|
|
|
const markdown = await getMarkdown();
|
2026-05-15 20:31:27 -07:00
|
|
|
assert(markdown === '> quoted text', `Expected "> quoted text", got: "${markdown}"`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('list item round-trips', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('- list item');
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown === '- list item', `Expected "- list item", got: "${markdown}"`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('nested inline in heading round-trips', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('# Hello **world**');
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown === '# Hello **world**', `Expected "# Hello **world**", got: "${markdown}"`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Enter key behaviour ────────────────────────────────────────────────────
|
2026-04-30 00:25:30 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
console.log('\nEnter key behaviour:');
|
2026-04-30 00:25:30 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('Enter splits current block into two blocks', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('hello');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('world');
|
|
|
|
|
const blocks = await getBlockClasses();
|
|
|
|
|
assert(blocks.length === 2, `Expected 2 blocks, got ${blocks.length}: ${JSON.stringify(blocks)}`);
|
2026-04-30 00:25:30 -07:00
|
|
|
const markdown = await getMarkdown();
|
2026-05-15 20:31:27 -07:00
|
|
|
assert(markdown === 'hello\nworld', `Expected "hello\\nworld", got: "${markdown}"`);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('Enter after heading creates new paragraph', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('# Title');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('body');
|
|
|
|
|
const blocks = await getBlockClasses();
|
|
|
|
|
assert(blocks.some(c => c.includes('md-h1')), `No h1 block: ${blocks}`);
|
|
|
|
|
assert(blocks.some(c => c.includes('md-paragraph')), `No paragraph block: ${blocks}`);
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown === '# Title\nbody', `Expected "# Title\\nbody", got: "${markdown}"`);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('Enter inside blockquote continues with > prefix', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('> first line');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('second line');
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(
|
|
|
|
|
markdown.includes('> first line'),
|
|
|
|
|
`Missing "> first line" in markdown: "${markdown}"`
|
|
|
|
|
);
|
|
|
|
|
assert(
|
|
|
|
|
markdown.includes('> second line'),
|
|
|
|
|
`Missing "> second line" — continuation prefix not added: "${markdown}"`
|
|
|
|
|
);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('Enter inside list item continues with - prefix', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('- first item');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('second item');
|
2026-04-30 00:25:30 -07:00
|
|
|
const markdown = await getMarkdown();
|
2026-05-15 20:31:27 -07:00
|
|
|
assert(
|
|
|
|
|
markdown.includes('- first item'),
|
|
|
|
|
`Missing "- first item": "${markdown}"`
|
|
|
|
|
);
|
|
|
|
|
assert(
|
|
|
|
|
markdown.includes('- second item'),
|
|
|
|
|
`Missing "- second item" — continuation prefix not added: "${markdown}"`
|
|
|
|
|
);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Backspace key behaviour ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
console.log('\nBackspace key behaviour:');
|
|
|
|
|
|
|
|
|
|
await test('Backspace at start of block merges with previous block', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('foo');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('bar');
|
|
|
|
|
await pressKey('Home');
|
|
|
|
|
await pressKey('Backspace');
|
|
|
|
|
const blocks = await getBlockClasses();
|
|
|
|
|
assert(blocks.length === 1, `Expected 1 block after merge, got ${blocks.length}`);
|
2026-04-30 00:25:30 -07:00
|
|
|
const markdown = await getMarkdown();
|
2026-05-15 20:31:27 -07:00
|
|
|
assert(markdown === 'foobar', `Expected "foobar", got: "${markdown}"`);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('Backspace mid-block does not merge', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('foo');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('bar');
|
|
|
|
|
await pressKey('Backspace');
|
|
|
|
|
const blocks = await getBlockClasses();
|
|
|
|
|
assert(blocks.length === 2, `Expected 2 blocks, got ${blocks.length}`);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Mode switching ─────────────────────────────────────────────────────────
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
console.log('\nMode switching:');
|
2026-04-29 11:12:45 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('view() switches to view state', async () => {
|
2026-04-29 11:12:45 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('**bold**');
|
|
|
|
|
await page.evaluate(() => window.__ribbitEditor.view());
|
|
|
|
|
await page.waitForTimeout(50);
|
|
|
|
|
const state = await page.evaluate(() => window.__ribbitEditor.getState());
|
|
|
|
|
assert(state === 'view', `Expected "view", got: "${state}"`);
|
2026-04-29 11:12:45 -07:00
|
|
|
});
|
2026-04-29 15:48:36 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('wysiwyg() switches back to wysiwyg state', async () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('hello');
|
|
|
|
|
await page.evaluate(() => window.__ribbitEditor.view());
|
|
|
|
|
await page.waitForTimeout(50);
|
|
|
|
|
await page.evaluate(() => window.__ribbitEditor.wysiwyg());
|
|
|
|
|
await page.waitForTimeout(50);
|
|
|
|
|
const state = await page.evaluate(() => window.__ribbitEditor.getState());
|
|
|
|
|
assert(state === 'wysiwyg', `Expected "wysiwyg", got: "${state}"`);
|
2026-04-29 15:48:36 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('content survives wysiwyg → view → wysiwyg round-trip', async () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('**bold** and *italic*');
|
|
|
|
|
const markdownBefore = await getMarkdown();
|
2026-04-29 15:48:36 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await page.evaluate(() => window.__ribbitEditor.view());
|
|
|
|
|
await page.waitForTimeout(50);
|
|
|
|
|
await page.evaluate(() => window.__ribbitEditor.wysiwyg());
|
|
|
|
|
await page.waitForTimeout(50);
|
2026-04-29 15:48:36 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
const markdownAfter = await getMarkdown();
|
|
|
|
|
assert(
|
|
|
|
|
markdownAfter === markdownBefore,
|
|
|
|
|
`Markdown changed after round-trip.\nBefore: "${markdownBefore}"\nAfter: "${markdownAfter}"`
|
|
|
|
|
);
|
2026-04-29 15:48:36 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('getMarkdown() returns source in view state', async () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('**bold**');
|
|
|
|
|
const markdownInEditor = await getMarkdown();
|
2026-04-29 15:48:36 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await page.evaluate(() => window.__ribbitEditor.view());
|
|
|
|
|
await page.waitForTimeout(50);
|
|
|
|
|
|
|
|
|
|
const markdownInView = await getMarkdown();
|
|
|
|
|
assert(
|
|
|
|
|
markdownInView === markdownInEditor,
|
|
|
|
|
`getMarkdown() changed on view switch.\nEditor: "${markdownInEditor}"\nView: "${markdownInView}"`
|
|
|
|
|
);
|
2026-04-29 15:48:36 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Complex documents ──────────────────────────────────────────────────────
|
2026-04-30 00:25:30 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
console.log('\nComplex documents:');
|
2026-04-30 00:25:30 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('multi-block document round-trips correctly', async () => {
|
2026-04-30 00:25:30 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('# Title');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('Some **bold** text.');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('> A quote');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('- A list item');
|
2026-04-30 00:25:30 -07:00
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown.includes('# Title'), `Missing heading: "${markdown}"`);
|
|
|
|
|
assert(markdown.includes('Some **bold** text.'), `Missing bold paragraph: "${markdown}"`);
|
|
|
|
|
assert(markdown.includes('> A quote'), `Missing blockquote: "${markdown}"`);
|
|
|
|
|
assert(markdown.includes('- A list item'), `Missing list item: "${markdown}"`);
|
2026-04-30 00:25:30 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
await test('empty lines between blocks preserved', async () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
await resetEditor();
|
2026-05-15 20:31:27 -07:00
|
|
|
await typeString('first');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await pressKey('Enter');
|
|
|
|
|
await typeString('second');
|
|
|
|
|
|
|
|
|
|
const blocks = await getBlockClasses();
|
|
|
|
|
assert(blocks.length === 3, `Expected 3 blocks (first, empty, second), got ${blocks.length}`);
|
|
|
|
|
const markdown = await getMarkdown();
|
|
|
|
|
assert(markdown === 'first\n\nsecond', `Expected "first\\n\\nsecond", got: "${markdown}"`);
|
2026-04-29 15:48:36 -07:00
|
|
|
});
|
2026-05-15 20:31:27 -07:00
|
|
|
*/
|
2026-04-29 11:12:45 -07:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 20:31:27 -07:00
|
|
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-29 11:12:45 -07:00
|
|
|
(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await setup();
|
|
|
|
|
await runTests();
|
|
|
|
|
} catch (error) {
|
2026-05-15 20:31:27 -07:00
|
|
|
console.error('\nSetup failed:', error.message);
|
2026-04-29 11:12:45 -07:00
|
|
|
failed++;
|
|
|
|
|
} finally {
|
2026-05-15 20:31:27 -07:00
|
|
|
const total = passed + failed;
|
|
|
|
|
console.log(`\n${passed}/${total} passed — ${failed} failed`);
|
2026-04-29 11:12:45 -07:00
|
|
|
if (errors.length) {
|
2026-05-15 20:31:27 -07:00
|
|
|
console.log('\nFailed tests:');
|
|
|
|
|
errors.forEach(({ name, message }) => {
|
|
|
|
|
console.log(` • ${name}`);
|
|
|
|
|
console.log(` ${message}`);
|
|
|
|
|
});
|
2026-04-29 11:12:45 -07:00
|
|
|
}
|
|
|
|
|
await teardown();
|
|
|
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
})();
|