Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aad0a37cfd | ||
|
|
50d3e7b476 | ||
|
|
bd62d906bb | ||
|
|
e4b46ddbfc | ||
|
|
deeec50b5f | ||
|
|
7eefdad512 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.94.0",
|
"version": "0.94.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/copy-vad-assets.mjs && vite",
|
"dev": "node scripts/copy-vad-assets.mjs && vite",
|
||||||
"build": "node scripts/copy-vad-assets.mjs && tsc && vite build",
|
"build": "node scripts/copy-vad-assets.mjs && tsc && vite build",
|
||||||
|
|||||||
@@ -104,6 +104,19 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The inner editable paragraph inherits `.ProseMirror p { margin: 0.5em 0 }`,
|
||||||
|
which pushes the first text line ~0.5em below the "N." marker (aligned to
|
||||||
|
flex-start), making the number float above the text. Drop the outer margins
|
||||||
|
so the marker and the first line share the same top edge — same approach
|
||||||
|
used for callouts in core.css. */
|
||||||
|
.definitionContent > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definitionContent > :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.backLink {
|
.backLink {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -10,9 +10,15 @@ ul[data-type="taskList"] {
|
|||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
> label {
|
> label {
|
||||||
padding-top: 0.2rem;
|
/* Box exactly one text-line tall and center the checkbox in it, so the
|
||||||
|
checkbox lines up with the first line of the item's text. This tracks
|
||||||
|
the editor line-height (--mantine-line-height-xl) instead of a magic
|
||||||
|
padding-top that drifts from the real line box. */
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
|
height: calc(var(--mantine-line-height-xl, 1.65) * 1em);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.94.0",
|
"version": "0.94.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import {
|
||||||
|
FastifyAdapter,
|
||||||
|
NestFastifyApplication,
|
||||||
|
} from '@nestjs/platform-fastify';
|
||||||
import * as request from 'supertest';
|
import * as request from 'supertest';
|
||||||
import { AppModule } from '../src/app.module';
|
import { AppModule } from '../src/app.module';
|
||||||
|
|
||||||
describe('AppController (e2e)', () => {
|
describe('AppController (e2e)', () => {
|
||||||
let app: INestApplication;
|
let app: NestFastifyApplication;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
// Docmost runs on Fastify (see src/main.ts). The default
|
||||||
|
// createNestApplication() would load @nestjs/platform-express, which is not
|
||||||
|
// a dependency of this project, so an explicit FastifyAdapter is required.
|
||||||
|
app = moduleFixture.createNestApplication<NestFastifyApplication>(
|
||||||
|
new FastifyAdapter(),
|
||||||
|
);
|
||||||
await app.init();
|
await app.init();
|
||||||
|
// Fastify must finish booting before its HTTP server can serve requests.
|
||||||
|
await app.getHttpAdapter().getInstance().ready();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Guard with optional chaining: if beforeEach throws before `app` is
|
||||||
|
// assigned, closing undefined would mask the original failure.
|
||||||
|
await app?.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('/ (GET)', () => {
|
it('/ (GET)', () => {
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
{
|
{
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
"moduleFileExtensions": ["js", "json", "ts", "tsx"],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
"testRegex": ".e2e-spec.ts$",
|
"testRegex": ".e2e-spec.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)sx?$": "ts-jest"
|
||||||
},
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"/node_modules/(?!(\\.pnpm/)?(nanoid|uuid|image-dimensions|marked|happy-dom|lib0|@sindresorhus[+/][a-z0-9-]+|escape-string-regexp|p-limit|yocto-queue)(@|/))"
|
||||||
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^@docmost/db/(.*)$": "<rootDir>/../src/database/$1",
|
"^@docmost/db/(.*)$": "<rootDir>/../src/database/$1",
|
||||||
"^@docmost/transactional/(.*)$": "<rootDir>/../src/integrations/transactional/$1",
|
"^@docmost/transactional/(.*)$": "<rootDir>/../src/integrations/transactional/$1",
|
||||||
"^@docmost/ee/(.*)$": "<rootDir>/../src/ee/$1"
|
"^@docmost/ee/(.*)$": "<rootDir>/../src/ee/$1",
|
||||||
|
"^src/(.*)$": "<rootDir>/../src/$1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.94.0",
|
"version": "0.94.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { writeFileSync, unlinkSync } from "node:fs";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { deflateSync } from "node:zlib";
|
import { deflateSync } from "node:zlib";
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
|
||||||
const API = process.env.DOCMOST_API_URL;
|
const API = process.env.DOCMOST_API_URL;
|
||||||
if (!API || !process.env.DOCMOST_EMAIL || !process.env.DOCMOST_PASSWORD) {
|
if (!API || !process.env.DOCMOST_EMAIL || !process.env.DOCMOST_PASSWORD) {
|
||||||
@@ -104,7 +105,7 @@ async function main() {
|
|||||||
{ find: "БУКВОЕД", replace: "КНИГОЛЮБ" },
|
{ find: "БУКВОЕД", replace: "КНИГОЛЮБ" },
|
||||||
{ find: "[1]", replace: "[42]" },
|
{ find: "[1]", replace: "[42]" },
|
||||||
]);
|
]);
|
||||||
check("edit_page_text: both edits applied", editRes.edits.every((e) => e.replacements === 1));
|
check("edit_page_text: both edits applied", editRes.applied.every((e) => e.replacements === 1));
|
||||||
await new Promise((r) => setTimeout(r, 16000)); // wait for server persistence
|
await new Promise((r) => setTimeout(r, 16000)); // wait for server persistence
|
||||||
const pj2 = await client.getPageJson(pageId);
|
const pj2 = await client.getPageJson(pageId);
|
||||||
const text2 = JSON.stringify(pj2.content);
|
const text2 = JSON.stringify(pj2.content);
|
||||||
@@ -149,11 +150,24 @@ async function main() {
|
|||||||
check("update_page_json: paragraph appended", JSON.stringify(pj4.content).includes("добавленный через update_page_json"));
|
check("update_page_json: paragraph appended", JSON.stringify(pj4.content).includes("добавленный через update_page_json"));
|
||||||
check("update_page_json: custom node id preserved", lastNode.attrs?.id === "testidjsonpush", lastNode.attrs?.id);
|
check("update_page_json: custom node id preserved", lastNode.attrs?.id === "testidjsonpush", lastNode.attrs?.id);
|
||||||
|
|
||||||
// 6b. images: upload / insert / replace (clean src, fresh attachment on replace)
|
// 6b. images: upload / insert / replace (clean src, fresh attachment on replace).
|
||||||
const pngA = join(tmpdir(), `mcp-e2e-img-a-${Date.now()}.png`);
|
// insert_image / replace_image take an http(s) URL that the SERVER fetches;
|
||||||
const pngB = join(tmpdir(), `mcp-e2e-img-b-${Date.now()}.png`);
|
// local file paths are intentionally unsupported. The Docmost server runs on
|
||||||
writeFileSync(pngA, makePng(255, 0, 0)); // red
|
// the same host as this test, so serve the PNG bytes over a throwaway
|
||||||
writeFileSync(pngB, makePng(0, 0, 255)); // blue (a DIFFERENT valid PNG)
|
// localhost HTTP server it can reach.
|
||||||
|
const bytesA = makePng(255, 0, 0); // red
|
||||||
|
const bytesB = makePng(0, 0, 255); // blue (a DIFFERENT valid PNG)
|
||||||
|
const imgServer = createServer((req, res) => {
|
||||||
|
res.writeHead(200, { "Content-Type": "image/png" });
|
||||||
|
res.end(req.url === "/b.png" ? bytesB : bytesA);
|
||||||
|
});
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
imgServer.once("error", reject);
|
||||||
|
imgServer.listen(0, "127.0.0.1", resolve);
|
||||||
|
});
|
||||||
|
const imgPort = imgServer.address().port;
|
||||||
|
const urlA = `http://127.0.0.1:${imgPort}/a.png`;
|
||||||
|
const urlB = `http://127.0.0.1:${imgPort}/b.png`;
|
||||||
try {
|
try {
|
||||||
// Independent login to fetch file bytes with the same cookie the editor uses.
|
// Independent login to fetch file bytes with the same cookie the editor uses.
|
||||||
const login = await axios.post(
|
const login = await axios.post(
|
||||||
@@ -173,7 +187,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// insert_image: append the first PNG, src must be clean (no ?v=) and fetchable.
|
// insert_image: append the first PNG, src must be clean (no ?v=) and fetchable.
|
||||||
const ins = await client.insertImage(pageId, pngA);
|
const ins = await client.insertImage(pageId, urlA);
|
||||||
check("insert_image: src has no ?v= cache-buster", !ins.src.includes("?v="), ins.src);
|
check("insert_image: src has no ?v= cache-buster", !ins.src.includes("?v="), ins.src);
|
||||||
const fileA = await fetchFile(ins.src);
|
const fileA = await fetchFile(ins.src);
|
||||||
check("insert_image: file fetch returns 200", fileA.status === 200, `status=${fileA.status}`);
|
check("insert_image: file fetch returns 200", fileA.status === 200, `status=${fileA.status}`);
|
||||||
@@ -199,7 +213,7 @@ async function main() {
|
|||||||
|
|
||||||
// replace_image: must create a NEW attachment with a clean, fetchable URL.
|
// replace_image: must create a NEW attachment with a clean, fetchable URL.
|
||||||
// The 200 fetch is the assertion that catches the in-place-overwrite HTTP 500 regression.
|
// The 200 fetch is the assertion that catches the in-place-overwrite HTTP 500 regression.
|
||||||
const rep = await client.replaceImage(pageId, oldAttachmentId, pngB);
|
const rep = await client.replaceImage(pageId, oldAttachmentId, urlB);
|
||||||
check("replace_image: new attachment id differs from old", rep.newAttachmentId !== oldAttachmentId, `${oldAttachmentId} -> ${rep.newAttachmentId}`);
|
check("replace_image: new attachment id differs from old", rep.newAttachmentId !== oldAttachmentId, `${oldAttachmentId} -> ${rep.newAttachmentId}`);
|
||||||
check("replace_image: src has no ?v= cache-buster", !rep.src.includes("?v="), rep.src);
|
check("replace_image: src has no ?v= cache-buster", !rep.src.includes("?v="), rep.src);
|
||||||
const fileB = await fetchFile(rep.src);
|
const fileB = await fetchFile(rep.src);
|
||||||
@@ -215,8 +229,7 @@ async function main() {
|
|||||||
check("replace_image: page has new attachment id", !!findImage(pjImg2.content.content, rep.newAttachmentId), rep.newAttachmentId);
|
check("replace_image: page has new attachment id", !!findImage(pjImg2.content.content, rep.newAttachmentId), rep.newAttachmentId);
|
||||||
check("replace_image: old attachment id repointed away", !findImage(pjImg2.content.content, oldAttachmentId), oldAttachmentId);
|
check("replace_image: old attachment id repointed away", !findImage(pjImg2.content.content, oldAttachmentId), oldAttachmentId);
|
||||||
} finally {
|
} finally {
|
||||||
try { unlinkSync(pngA); } catch {}
|
imgServer.close();
|
||||||
try { unlinkSync(pngB); } catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6c. rich formatting: callout type, task list, inline marks, table alignment,
|
// 6c. rich formatting: callout type, task list, inline marks, table alignment,
|
||||||
@@ -441,7 +454,10 @@ async function main() {
|
|||||||
|
|
||||||
// 9. comments: create / list / reply / update / check_new / delete
|
// 9. comments: create / list / reply / update / check_new / delete
|
||||||
const beforeComments = new Date(Date.now() - 1000).toISOString();
|
const beforeComments = new Date(Date.now() - 1000).toISOString();
|
||||||
const c1 = await client.createComment(pageId, "Первый **комментарий** с [ссылкой](https://example.com).");
|
// A top-level comment requires an inline "selection": exact contiguous text
|
||||||
|
// that exists in the persisted page to anchor on. "Добавленный абзац." is a
|
||||||
|
// plain paragraph re-imported in section 5 and still present here.
|
||||||
|
const c1 = await client.createComment(pageId, "Первый **комментарий** с [ссылкой](https://example.com).", "inline", "Добавленный абзац.");
|
||||||
check("create_comment: created", !!c1.data.id, c1.data.id);
|
check("create_comment: created", !!c1.data.id, c1.data.id);
|
||||||
check("create_comment: markdown round-trip", c1.data.content.includes("**комментарий**"), c1.data.content);
|
check("create_comment: markdown round-trip", c1.data.content.includes("**комментарий**"), c1.data.content);
|
||||||
const reply = await client.createComment(pageId, "Ответ на комментарий.", "page", undefined, c1.data.id);
|
const reply = await client.createComment(pageId, "Ответ на комментарий.", "page", undefined, c1.data.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user