ribbit/test/integration/test.js

291 lines
11 KiB
JavaScript
Raw Normal View History

/**
* Integration tests for the ribbit editor using Selenium + Firefox.
*
* Run: npm run test:e2e
*/
const { Builder, By, Key, until } = require('selenium-webdriver');
const firefox = require('selenium-webdriver/firefox');
const { createServer } = require('./server');
let server;
let driver;
async function setup() {
server = createServer(9999);
await server.start();
const options = new firefox.Options().addArguments('--headless');
driver = await new Builder()
.forBrowser('firefox')
.setFirefoxOptions(options)
.build();
await driver.get(server.url);
// Wait for ribbit to initialize
await driver.wait(async () => {
return driver.executeScript('return window.__ribbitReady === true');
}, 10000).catch(async () => {
const logs = await driver.manage().logs().get('browser').catch(() => []);
console.log('Browser logs:', logs.map(l => l.message));
const ready = await driver.executeScript('return { ready: window.__ribbitReady, ribbit: typeof window.ribbit, editor: typeof window.__ribbitEditor }');
console.log('State:', ready);
throw new Error('Editor did not become ready');
});
}
async function teardown() {
if (driver) await driver.quit();
if (server) await server.stop();
}
// Test helpers
async function getEditorHTML() {
return driver.executeScript('return document.getElementById("ribbit").innerHTML');
}
async function getEditorText() {
return driver.executeScript('return document.getElementById("ribbit").textContent');
}
async function getState() {
return driver.executeScript('return window.__ribbitEditor.getState()');
}
async function clickButton(label) {
const buttons = await driver.findElements(By.css('.ribbit-toolbar button'));
for (const btn of buttons) {
const text = await btn.getText();
if (text === label) {
await btn.click();
return;
}
}
throw new Error(`Button "${label}" not found`);
}
async function clickEditor() {
const editor = await driver.findElement(By.id('ribbit'));
await editor.click();
}
// Test runner
let passed = 0;
let failed = 0;
const errors = [];
async function test(name, fn) {
try {
await fn();
passed++;
console.log(`${name}`);
} catch (e) {
failed++;
errors.push(name);
console.log(`${name}`);
console.log(` ${e.message}`);
}
}
function assert(condition, message) {
if (!condition) throw new Error(message || 'Assertion failed');
}
// Tests
async function runTests() {
console.log('\nRibbit Integration Tests\n');
await test('page loads', async () => {
const title = await driver.getTitle();
assert(title === 'Ribbit Integration Test Page', `Title: ${title}`);
});
await test('editor renders in view mode', async () => {
const state = await getState();
assert(state === 'view', `State: ${state}`);
});
await test('editor renders markdown as HTML', async () => {
const html = await getEditorHTML();
assert(html.includes('<strong>bold</strong>'), 'Missing bold');
assert(html.includes('<em>italic</em>'), 'Missing italic');
assert(html.includes('<code>code</code>'), 'Missing code');
});
await test('editor renders headings', async () => {
const html = await getEditorHTML();
assert(html.includes('<h2'), 'Missing h2');
});
await test('editor renders lists', async () => {
const html = await getEditorHTML();
assert(html.includes('<ul>'), 'Missing ul');
assert(html.includes('<li>'), 'Missing li');
});
await test('editor renders tables', async () => {
const html = await getEditorHTML();
assert(html.includes('<table>'), 'Missing table');
});
await test('editor renders blockquotes', async () => {
const html = await getEditorHTML();
assert(html.includes('<blockquote>'), 'Missing blockquote');
});
await test('toolbar is rendered', async () => {
const toolbar = await driver.findElements(By.css('.ribbit-toolbar'));
assert(toolbar.length > 0, 'No toolbar found');
});
await test('toolbar has buttons with labels', async () => {
const buttons = await driver.findElements(By.css('.ribbit-toolbar button'));
assert(buttons.length > 5, `Only ${buttons.length} buttons`);
const text = await buttons[0].getText();
assert(text.length > 0, 'Button has no label');
});
await test('toggle button switches to wysiwyg', async () => {
await clickButton('Edit');
const state = await getState();
assert(state === 'wysiwyg', `State: ${state}`);
});
await test('editor is contentEditable in wysiwyg', async () => {
const editable = await driver.executeScript(
'return document.getElementById("ribbit").contentEditable'
);
assert(editable === 'true', `contentEditable: ${editable}`);
});
await test('can type in wysiwyg mode', async () => {
await clickEditor();
// Move to end and type
await driver.actions().keyDown(Key.CONTROL).sendKeys(Key.END).keyUp(Key.CONTROL).perform();
await driver.actions().sendKeys('\nhello from selenium').perform();
const text = await getEditorText();
assert(text.includes('hello from selenium'), 'Typed text not found');
});
await test('source button switches to edit mode', async () => {
await clickButton('Source');
const state = await getState();
assert(state === 'edit', `State: ${state}`);
});
await test('edit mode shows raw markdown', async () => {
const text = await getEditorText();
assert(text.includes('**bold**'), 'Missing raw markdown');
});
await test('toggle back to view mode', async () => {
await clickButton('Edit');
const state = await getState();
assert(state === 'view', `State: ${state}`);
});
await test('view mode renders HTML again', async () => {
const html = await getEditorHTML();
assert(html.includes('<strong>bold</strong>'), 'Not rendered as HTML');
});
await test('save button fires save event', async () => {
await driver.executeScript('window.__saved = false; window.__ribbitEditor.on("save", () => { window.__saved = true; })');
await clickButton('Edit');
await clickButton('Save');
const saved = await driver.executeScript('return window.__saved');
assert(saved === true, 'Save event not fired');
});
await test('enter key creates new line in wysiwyg', async () => {
await driver.executeScript('window.__ribbitEditor.wysiwyg()');
await clickEditor();
// Clear and type two lines
await driver.actions().keyDown(Key.CONTROL).sendKeys('a').keyUp(Key.CONTROL).perform();
await driver.actions().sendKeys(Key.DELETE).perform();
await driver.actions().sendKeys('line one').perform();
await driver.actions().sendKeys(Key.ENTER).perform();
await driver.actions().sendKeys('line two').perform();
const text = await getEditorText();
assert(text.includes('line one'), `Missing "line one" in: ${text}`);
assert(text.includes('line two'), `Missing "line two" in: ${text}`);
// Check that they're on separate lines (not concatenated)
const html = await getEditorHTML();
const hasBreak = html.includes('<br') || html.includes('<div') || html.includes('<p');
assert(hasBreak, `No line break in HTML: ${html}`);
});
await test('enter key in wysiwyg produces valid markdown', async () => {
// Get the markdown from the content typed above
const md = await driver.executeScript('return window.__ribbitEditor.getMarkdown()');
assert(md.includes('line one'), `Missing "line one" in markdown: ${md}`);
assert(md.includes('line two'), `Missing "line two" in markdown: ${md}`);
// Lines should be separate (not on same line)
const lines = md.split('\n').filter(l => l.trim());
const hasLineOne = lines.some(l => l.includes('line one'));
const hasLineTwo = lines.some(l => l.includes('line two'));
assert(hasLineOne, `"line one" not on its own line in: ${md}`);
assert(hasLineTwo, `"line two" not on its own line in: ${md}`);
});
await test('multiple enters create blank lines in wysiwyg', async () => {
await driver.executeScript('window.__ribbitEditor.wysiwyg()');
await clickEditor();
await driver.actions().keyDown(Key.CONTROL).sendKeys('a').keyUp(Key.CONTROL).perform();
await driver.actions().sendKeys(Key.DELETE).perform();
await driver.actions().sendKeys('para one').perform();
await driver.actions().sendKeys(Key.ENTER, Key.ENTER).perform();
await driver.actions().sendKeys('para two').perform();
const text = await getEditorText();
assert(text.includes('para one'), `Missing "para one" in: ${text}`);
assert(text.includes('para two'), `Missing "para two" in: ${text}`);
});
await test('enter after heading in wysiwyg', async () => {
await driver.executeScript('window.__ribbitEditor.wysiwyg()');
await clickEditor();
await driver.actions().keyDown(Key.CONTROL).sendKeys('a').keyUp(Key.CONTROL).perform();
await driver.actions().sendKeys(Key.DELETE).perform();
await driver.actions().sendKeys('## My Heading').perform();
await driver.actions().sendKeys(Key.ENTER).perform();
await driver.actions().sendKeys('paragraph text').perform();
const md = await driver.executeScript('return window.__ribbitEditor.getMarkdown()');
assert(md.includes('Heading') || md.includes('heading'), `Missing heading in: ${md}`);
assert(md.includes('paragraph'), `Missing paragraph in: ${md}`);
});
await test('Ctrl+B shortcut works in wysiwyg', async () => {
// Switch to wysiwyg
await driver.executeScript('window.__ribbitEditor.wysiwyg()');
await clickEditor();
// Type and select
await driver.actions().sendKeys('test text').perform();
await driver.actions()
.keyDown(Key.SHIFT)
.sendKeys(Key.ARROW_LEFT, Key.ARROW_LEFT, Key.ARROW_LEFT, Key.ARROW_LEFT)
.keyUp(Key.SHIFT)
.perform();
// Ctrl+B
await driver.actions().keyDown(Key.CONTROL).sendKeys('b').keyUp(Key.CONTROL).perform();
const html = await getEditorHTML();
assert(html.includes('**'), 'Bold delimiter not inserted');
});
}
(async () => {
try {
await setup();
await runTests();
} catch (e) {
console.error('Setup failed:', e.message);
failed++;
} finally {
console.log(`\n${passed}/${passed + failed} passed — ${failed} failed`);
if (errors.length) {
console.log('\nFailed:');
errors.forEach(e => console.log(`${e}`));
}
await teardown();
process.exit(failed > 0 ? 1 : 0);
}
})();