/** * Invariant: a plugin's new-file type appears in the tab bar + menu, creates an untitled file of * its extension at the workspace root seeded with its template, and opens it in the plugin's * custom editor, whose tab shows the editor's icon and hover card its name. */ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { deflateSync } from 'node:zlib' import { PLUGIN_MANIFEST_V2_SINCE } from '../../src/shared/plugins/manifest/plugin-schema-field-docs' import { expect, test } from './helpers/orca-app' import { disablePluginSystem, enableDevPlugins } from './helpers/plugin-smoke' const TEMPLATE = '{"type":"sketch","elements":[]}\n' const FRAME = 'iframe[title="Sketch Pad"]' const EDITOR_HTML = ` ` function crc32(bytes: Buffer): number { let crc = ~0 for (const byte of bytes) { crc ^= byte for (let bit = 0; bit < 8; bit += 1) { crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1 } } return ~crc >>> 0 } function pngChunk(type: string, data: Buffer): Buffer { const body = Buffer.concat([Buffer.from(type, 'ascii'), data]) const length = Buffer.alloc(4) length.writeUInt32BE(data.length) const crc = Buffer.alloc(4) crc.writeUInt32BE(crc32(body)) return Buffer.concat([length, body, crc]) } /** A square RGB PNG, the size plugin icons must be. */ function solidPng(edge: number, rgb: [number, number, number]): Buffer { const header = Buffer.alloc(13) header.writeUInt32BE(edge, 0) header.writeUInt32BE(edge, 4) header[8] = 8 header[9] = 2 const row = Buffer.concat([ Buffer.from([0]), Buffer.concat(Array.from({ length: edge }, () => Buffer.from(rgb))) ]) return Buffer.concat([ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), pngChunk('IHDR', header), pngChunk('IDAT', deflateSync(Buffer.concat(Array.from({ length: edge }, () => row)))), pngChunk('IEND', Buffer.alloc(0)) ]) } async function writeSketchPlugin(root: string): Promise { await mkdir(join(root, 'editor'), { recursive: true }) await mkdir(join(root, 'templates'), { recursive: true }) await writeFile(join(root, 'editor', 'index.html'), EDITOR_HTML) await writeFile(join(root, 'templates', 'empty.sketch'), TEMPLATE) await writeFile(join(root, 'tab.png'), solidPng(128, [0x6d, 0x5d, 0xfc])) await writeFile( join(root, 'orca-plugin.json'), JSON.stringify({ manifestVersion: 2, execution: 'sandboxed-ui', id: 'sketch-pad', publisher: 'samples', name: 'Sketch Pad', version: '1.0.0', engines: { orca: `>=${PLUGIN_MANIFEST_V2_SINCE}` }, contributes: { customEditors: [ { id: 'sketch', title: 'Sketch Pad', icon: 'tab.png', extensions: ['.sketch'], entry: 'editor/index.html' } ], newFiles: [ { id: 'sketch', title: 'New sketch', extension: '.sketch', template: 'templates/empty.sketch', icon: 'tab.png' } ] } }) ) } test('a plugin new file is created from the + menu and opens in its editor', async ({ orcaPage, seededRepoPath }) => { const pluginRoot = await mkdtemp(join(tmpdir(), 'orca-new-file-')) const filePath = join(seededRepoPath, 'untitled.sketch') try { await writeSketchPlugin(pluginRoot) await enableDevPlugins(orcaPage, [pluginRoot]) await orcaPage.getByRole('button', { name: 'New tab' }).first().click() const item = orcaPage.getByRole('menuitem', { name: 'New sketch' }) await expect(item).toBeVisible({ timeout: 15_000 }) await expect(item.locator('img[src^="orca-plugin-image://"]')).toBeVisible() await item.click() await expect.poll(() => readFile(filePath, 'utf8').catch(() => null)).toBe(TEMPLATE) await expect(orcaPage.frameLocator(FRAME).locator('#doc')).toHaveValue(TEMPLATE, { timeout: 15_000 }) const tab = orcaPage.locator('[data-tab-id]').filter({ hasText: 'untitled.sketch' }).first() const icon = tab.locator('img[src*="/editor-0"]') await expect(icon).toBeVisible() } finally { await disablePluginSystem(orcaPage).catch(() => undefined) await rm(filePath, { force: true }) await rm(pluginRoot, { recursive: true, force: true }) } })