diff --git a/frontend/e2e/default-editor.spec.js b/frontend/e2e/default-editor.spec.js index 497eee5..a85b0fc 100644 --- a/frontend/e2e/default-editor.spec.js +++ b/frontend/e2e/default-editor.spec.js @@ -36,6 +36,69 @@ test.describe('F: Default Editor Plugin', () => { await expect(textarea).toHaveValue('Buy groceries\nWrite tests'); }); + test('secret link opens its exact secret and closing it restores the note preview', async ({ page }) => { + const notePath = 'Notes/Secret Link.md'; + const noteContent = '# Secret link\n\n[Target secret](verstak-secret://target.secret)'; + + await page.evaluate(async ({ notePath, noteContent }) => { + const writeError = await window.go.api.App.WriteVaultTextFile( + 'verstak.platform-test', + notePath, + noteContent, + { createIfMissing: true, overwrite: true }, + ); + if (writeError) throw new Error(writeError); + const [result, openError] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', { + kind: 'vault-file', + path: notePath, + extension: '.md', + context: { sourceView: 'notes', isInsideNotesFolder: true, notesMode: true }, + }); + if (openError) throw new Error(openError); + window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result })); + }, { notePath, noteContent }); + + const note = page.locator('[data-editor-mode="notes-markdown"]'); + await expect(note).toBeVisible({ timeout: 10000 }); + await expect(note.locator('[data-preview]')).toContainText('Target secret'); + await note.locator('.secret-link').click(); + + const secrets = page.locator('.secrets-root'); + await expect(secrets).toBeVisible({ timeout: 10000 }); + await expect(secrets.locator('.secrets-item.active .secrets-item-title')).toHaveText('Target secret'); + await expect(secrets.locator('.secrets-card h2')).toHaveText('Target secret'); + + await page.locator('.workbench-host .close-btn').click(); + await expect(note).toBeVisible({ timeout: 10000 }); + await expect(note.locator('[data-preview]')).toContainText('Target secret'); + await expect(note.locator('[data-editor-textarea]')).toHaveCount(0); + await expect(note.locator('[data-save-state]')).toHaveText(''); + + const storedContent = await page.evaluate(async ({ notePath }) => { + const [content, readError] = await window.go.api.App.ReadVaultTextFile('verstak.platform-test', notePath); + if (readError) throw new Error(readError); + return content; + }, { notePath }); + expect(storedContent).toBe(noteContent); + }); + + test('unavailable secret does not fall back to the first secret', async ({ page }) => { + await page.evaluate(async () => { + const [result, openError] = await window.go.api.App.OpenWorkbenchResource('verstak.default-editor', { + kind: 'secret', + path: 'missing.secret', + mode: 'view', + }); + if (openError) throw new Error(openError); + window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result })); + }); + + const secrets = page.locator('.secrets-root'); + await expect(secrets).toBeVisible({ timeout: 10000 }); + await expect(secrets.locator('.secrets-item.active')).toHaveCount(0); + await expect(secrets.locator('.secrets-status.error')).toContainText(/unavailable|недоступен/i); + }); + test('editor supports markdown toolbar split save reopen and revert', async ({ page }) => { await page.evaluate(async () => { const err = await window.go.api.App.WriteVaultTextFile( diff --git a/frontend/src/lib/test/wails-mock.js b/frontend/src/lib/test/wails-mock.js index eba2c40..c130f2f 100644 --- a/frontend/src/lib/test/wails-mock.js +++ b/frontend/src/lib/test/wails-mock.js @@ -4,6 +4,9 @@ * Каждый метод возвращает Promise с данными, совместимыми с Wails-контрактом. * Состояние мутабельно — тесты могут менять его между сценариями. */ +import defaultEditorSource from '../../../../../verstak-official-plugins/plugins/default-editor/frontend/src/index.js?raw'; +import secretsSource from '../../../../../verstak-official-plugins/plugins/secrets/frontend/src/index.js?raw'; + (function () { if (window.__wailsMockReady) return; @@ -360,6 +363,7 @@ 'verstak.platform-test': { savedText: 'initial value' } }; var pluginData = {}; + var secretRecords = makeDefaultSecretRecords(); var vaultFiles = makeDefaultVaultFiles(); var externalOpens = []; var trashEntries = []; @@ -404,6 +408,27 @@ return JSON.parse(JSON.stringify(value)); } + function makeDefaultSecretRecords() { + return [ + { + id: 'first.secret', + title: 'First secret', + username: 'first-user', + value: 'first-value', + scope: { kind: 'global' }, + updatedAt: '2026-07-14T00:00:00Z' + }, + { + id: 'target.secret', + title: 'Target secret', + username: 'target-user', + value: 'target-value', + scope: { kind: 'global' }, + updatedAt: '2026-07-14T00:00:00Z' + } + ]; + } + function builtInWorkspaceTemplates() { return [ { @@ -573,8 +598,15 @@ icon: 'key-round', provides: ['secret-store', 'secrets.read-ui', 'secrets.write-ui'], permissions: ['secrets.read', 'secrets.write', 'ui.register'], - frontend: { entry: 'frontend/dist/index.js' }, + frontend: { entry: 'frontend/src/index.js' }, contributes: { + openProviders: [{ + id: 'verstak.secrets.secret', + title: 'Secrets', + priority: 100, + component: 'SecretsView', + supports: [{ kind: 'secret', modes: ['view'] }] + }], workspaceItems: [{ id: 'verstak.secrets.workspace', title: 'Secrets', icon: 'key-round', component: 'SecretsView' }] } }, @@ -3328,6 +3360,43 @@ GetPluginLocalization: function (pluginId, locale) { return Promise.resolve([mockPluginCatalog(pluginId, locale), '']); }, + PluginSecretsStatus: function () { + return Promise.resolve([{ initialized: true, unlocked: true }, '']); + }, + PluginSecretsUnlock: function () { + return Promise.resolve(''); + }, + PluginSecretsList: function () { + return Promise.resolve([secretRecords.map(function (record) { + var listed = cloneJson(record); + delete listed.value; + return listed; + }), '']); + }, + PluginSecretsRead: function (_pluginId, secretID) { + var record = secretRecords.find(function (item) { return item.id === secretID; }); + if (!record) return Promise.resolve([{}, 'not-found: secret ' + secretID]); + return Promise.resolve([cloneJson(record), '']); + }, + PluginSecretsWrite: function (_pluginId, nextRecord) { + var record = Object.assign({}, nextRecord || {}); + if (!record.id) return Promise.resolve([{}, 'secret id is required']); + record.scope = record.scope || { kind: 'global' }; + record.updatedAt = new Date().toISOString(); + var index = secretRecords.findIndex(function (item) { return item.id === record.id; }); + if (index === -1) secretRecords.push(record); + else secretRecords[index] = record; + return Promise.resolve([cloneJson(record), '']); + }, + PluginSecretsDelete: function (_pluginId, secretID) { + secretRecords = secretRecords.filter(function (record) { return record.id !== secretID; }); + return Promise.resolve(''); + }, + PluginSecretsCopyLink: function (_pluginId, secretID) { + var record = secretRecords.find(function (item) { return item.id === secretID; }); + if (!record) return Promise.resolve(['', 'not-found: secret ' + secretID]); + return Promise.resolve(['[' + (record.title || record.id) + '](verstak-secret://' + encodeURIComponent(record.id) + ')', '']); + }, ReadPluginSettings: function (pluginId) { return Promise.resolve([Object.assign({}, pluginSettings[pluginId] || {}), '']); }, @@ -3455,8 +3524,8 @@ if (pluginId === 'verstak.platform-test' && assetPath === 'frontend/dist/index.js') { return Promise.resolve(platformTestBundle()); } - if (pluginId === 'verstak.default-editor' && assetPath === 'frontend/dist/index.js') { - return Promise.resolve(defaultEditorBundle()); + if (pluginId === 'verstak.default-editor') { + return Promise.resolve(defaultEditorSource); } if (pluginId === 'verstak.files' && assetPath === 'frontend/dist/index.js') { return Promise.resolve(filesPluginBundle()); @@ -3479,8 +3548,8 @@ if (pluginId === 'verstak.todo' && assetPath === 'frontend/dist/index.js') { return Promise.resolve(todoBundle()); } - if (pluginId === 'verstak.secrets' && assetPath === 'frontend/dist/index.js') { - return Promise.resolve(simplePluginBundle('verstak.secrets', 'SecretsView', 'secrets-root', 'Secrets')); + if (pluginId === 'verstak.secrets') { + return Promise.resolve(secretsSource); } if (pluginId === 'verstak.search' && assetPath === 'frontend/dist/index.js') { return Promise.resolve(searchPluginBundle()); @@ -4164,6 +4233,7 @@ workbenchPreferences = {}; openedResources = []; pluginSettings = { 'verstak.platform-test': { savedText: 'initial value' } }; + secretRecords = makeDefaultSecretRecords(); vaultFiles = makeDefaultVaultFiles(); externalOpens = []; trashEntries = []; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index de13ef2..6d78fe3 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,3 +1,4 @@ +import { resolve } from 'node:path'; import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; @@ -13,6 +14,9 @@ export default defineConfig(({ mode }) => { server: { port: isTest ? 5174 : 5173, strictPort: true, + fs: { + allow: [resolve(__dirname, '..', '..')], + }, }, optimizeDeps: { include: [