import assert from 'node:assert/strict' import { readFile, mkdir } from 'node:fs/promises' import { createHash } from 'node:crypto' import { chromium } from 'playwright' if (!process.env.PLAYWRIGHT_CHROMIUM_BIN) throw new Error('Run in the Nix shell with PLAYWRIGHT_CHROMIUM_BIN') const url = process.env.REAMS_CATALOG_TEST_URL || 'http://localhost:7051' const candidate = process.env.CANDIDATE_PLUGIN const artifacts = '.invariant-artifacts/plugin-catalog' await mkdir(artifacts, { recursive: true }) const browser = await chromium.launch({ executablePath: process.env.PLAYWRIGHT_CHROMIUM_BIN, headless: true, args: ['--no-sandbox'] }) const context = await browser.newContext() const page = await context.newPage() const errors = [] const consoleErrors = [] page.on('pageerror', error => errors.push(error.message)) page.on('response', response => { if (response.status() >= 400) console.error('Failed response:', response.status(), response.url()) }) page.on('console', message => { if (message.type() === 'error' && !message.text().includes('net::ERR_FAILED') && !message.location().url.endsWith('/favicon.ico')) { consoleErrors.push(message.text()) console.error('Browser console:', message.text(), message.location()) } }) const original = JSON.parse(await readFile('public/plugin-catalog/catalog.json', 'utf8')) if (candidate) { const directory = `public/test-plugins/${candidate}` const manifest = JSON.parse(await readFile(`${directory}/manifest.json`, 'utf8')) const files = {} for (const file of ['main.js', 'styles.css']) { try { files[file] = createHash('sha256').update(await readFile(`${directory}/${file}`)).digest('hex') } catch (error) { if (file !== 'styles.css' || error.code !== 'ENOENT') throw error } } const bundlePath = `bundles/${manifest.id}/${manifest.version}/` original.plugins = original.plugins.filter(entry => entry.manifest.id !== manifest.id) original.plugins.push({ manifest, files, bundlePath, waitForMetadata: candidate === 'datacore', notes: 'Candidate under validation', testedAt: '2026-10-10', repository: `https://github.com/${candidate === 'datacore' ? 'blacksmithgu/datacore' : 'argenos/nldates-obsidian'}` }) await page.route(`**/${bundlePath}*`, async route => { const file = new URL(route.request().url()).pathname.split('/').pop() await route.fulfill({ body: await readFile(`${directory}/${file}`), contentType: file.endsWith('.css') ? 'text/css' : 'text/javascript' }) }) } if (candidate) await page.route('**/plugin-catalog/catalog.json', route => route.fulfill({ json: original })) const entry = candidate ? original.plugins.at(-1) : original.plugins.find(entry => entry.manifest.id === (process.env.PLUGIN_ID || 'emoji-shortcodes')) assert.ok(entry, 'Requested plugin exists in the catalog') const name = entry.manifest.name const id = entry.manifest.id async function openSettings() { await page.getByRole('button', { name: 'Open settings', exact: true }).click() await page.getByRole('tab', { name: 'Obsidian Plugins', exact: true }).click() await page.getByRole('switch', { name: `Toggle ${name}`, exact: true }).waitFor({ state: 'attached' }) } async function active() { await page.waitForFunction(id => window.__reamsApp?._pluginLoader.getPluginState(id) === 'active', id, { timeout: 15000 }) } async function setSourceMode(enabled) { await page.getByRole('button', { name: 'Open settings', exact: true }).click() await page.getByRole('tab', { name: 'Editor', exact: true }).click() const toggle = page.getByRole('switch', { name: 'Source mode', exact: true }) if (await toggle.isChecked() !== enabled) await toggle.locator('..').click() await page.getByRole('button', { name: 'Close settings', exact: true }).click() } async function replaceNote(content) { const editor = page.locator('.cm-content[contenteditable=true]').first() await editor.click() await page.keyboard.press('Control+a') await page.keyboard.insertText(content) await page.keyboard.press('Control+End') } async function createNote(name, content) { await page.getByRole('button', { name: 'New file', exact: true }).click() await page.getByPlaceholder('my-note or folder/my-note').fill(name) const template = page.getByRole('combobox', { name: 'Template', exact: true }) if (await template.count()) await template.selectOption('') await page.locator('.create-item-modal button[type=submit]').click() await page.getByRole('tab', { name, exact: true }).waitFor() await replaceNote(content) } async function openPlaygroundNote(path) { await page.locator(`.cm-wikilink-chip[data-target="Datacore Playground/${path}"]`).click() await page.waitForFunction(path => window.__reamsApp.workspace.getActiveFile()?.path === `Datacore Playground/${path}.md`, path) } try { await page.goto(url) await page.getByRole('button', { name: 'Open settings', exact: true }).waitFor({ timeout: 60000 }) await openSettings() assert.equal(await page.getByRole('note').textContent().then(text => text.includes('Request more plugins in the feedback UI.')), true) assert.equal(await page.getByRole('switch', { name: 'Toggle Periodic Notes', exact: true }).count(), 0) assert.equal(await page.getByRole('switch', { name: 'Toggle Recent Files', exact: true }).count(), 0) assert.equal(await page.getByText('Not installed', { exact: true }).count(), 0) assert.equal(await page.getByRole('button', { name: 'Add test corpus', exact: true }).count(), 0) const gaps = await page.locator('.settings-catalog-card').first().evaluate(card => { const items = [...card.querySelectorAll('.settings-plugin-name, .settings-plugin-description, .settings-plugin-version, .settings-catalog-notes')] return items.slice(1).map((item, i) => item.getBoundingClientRect().top - items[i].getBoundingClientRect().bottom) }) assert.deepEqual(gaps, [4, 4, 4], 'Card copy has consistent vertical gaps') await page.getByRole('switch', { name: `Toggle ${name}`, exact: true }).locator('..').click() await active() if (id === 'datacore') await page.waitForFunction(() => window.datacore?.core.initialized) await page.screenshot({ path: `${artifacts}/${id}-installed.png` }) assert.equal(await page.getByRole('switch', { name: `Toggle ${name}`, exact: true }).isChecked(), true) assert.equal(await page.getByRole('button', { name: 'Add test corpus', exact: true }).count(), 0) await page.getByRole('switch', { name: `Toggle ${name}`, exact: true }).locator('..').click() await page.waitForFunction(id => window.__reamsApp?._pluginLoader.getPluginState(id) === 'disabled', id) assert.equal(await page.evaluate(id => !!document.getElementById(`reams-plugin-styles-${id}`), id), false) await page.getByRole('switch', { name: `Toggle ${name}`, exact: true }).locator('..').click() await active() await page.getByRole('tab', { name, exact: true }).click() assert.ok(await page.locator('[role=tabpanel] input, [role=tabpanel] select').count(), 'Plugin settings render') await page.getByRole('button', { name: 'Close settings', exact: true }).click() if (id === 'nldates-obsidian') { const expected = await page.evaluate(() => window.moment().add(1, 'day').format('YYYY-MM-DD')) assert.equal(await page.evaluate(() => window.__reamsApp.plugins.getPlugin('nldates-obsidian').parseDate('tomorrow').formattedString), expected) await createNote('catalog-date-test', '') const editor = page.locator('.cm-content[contenteditable=true]').first() await editor.click() await page.keyboard.type('@tomorrow', { delay: 60 }) await page.locator('.cm-editor-suggest-dropdown').waitFor() await page.screenshot({ path: `${artifacts}/${id}-suggestion.png` }) await page.keyboard.press('Enter') await page.waitForFunction(expected => document.querySelector('.cm-content')?.textContent.includes(expected), expected) const note = await page.evaluate(async () => window.__reamsApp.vault.read(window.__reamsApp.vault.getAbstractFileByPath('catalog-date-test.md'))) assert.ok(note.includes(expected), 'Date insertion writes the actual note') } if (id === 'datacore') { await page.waitForFunction(() => window.datacore?.query('@page').length > 0, null, { timeout: 15000 }) await openSettings() await page.getByRole('switch', { name: 'Toggle Dataview', exact: true }).locator('..').click() await page.waitForFunction(() => !!window.DataviewAPI) await page.getByRole('button', { name: 'Close settings', exact: true }).click() await setSourceMode(true) await createNote('datacore-data', '---\nstatus: active\ntags: [catalog-probe]\n---\n\nTest data\n\n- [ ] Ship the query\n') const query = '# Query comparison\n\n```datacorejsx\nreturn function View() { const rows = dc.useQuery("@page and #catalog-probe"); const columns = [{id:"File", value:r=>r.$link},{id:"Status",value:r=>r.field("status")?.value}]; return