Emdash source with visual editor image upload fix

Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
2026-05-03 10:44:54 +07:00
parent 78f81bebb6
commit 2d1be52177
2352 changed files with 662964 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {
"name": "E2E Test Fixture",
"description": "Schema for E2E tests"
},
"taxonomies": [
{
"name": "category",
"label": "Categories",
"labelSingular": "Category",
"hierarchical": true,
"collections": ["posts"],
"terms": [
{ "slug": "news", "label": "News" },
{ "slug": "tutorials", "label": "Tutorials" },
{ "slug": "opinion", "label": "Opinion" }
]
},
{
"name": "tag",
"label": "Tags",
"labelSingular": "Tag",
"hierarchical": false,
"collections": ["posts"]
}
],
"sections": [
{
"slug": "hero",
"title": "Hero Section",
"description": "Main hero area",
"content": [
{
"_type": "block",
"_key": "b1",
"style": "normal",
"children": [{ "_type": "span", "_key": "s1", "text": "Welcome to our site" }],
"markDefs": []
}
]
}
],
"collections": [
{
"slug": "posts",
"label": "Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "search"],
"commentsEnabled": true,
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true,
"searchable": true
},
{
"slug": "featured_image",
"label": "Featured Image",
"type": "image"
},
{
"slug": "body",
"label": "Body",
"type": "portableText",
"searchable": true
},
{
"slug": "excerpt",
"label": "Excerpt",
"type": "text",
"searchable": true
},
{
"slug": "theme_color",
"label": "Theme Color",
"type": "string",
"widget": "color:picker"
}
]
},
{
"slug": "pages",
"label": "Pages",
"labelSingular": "Page",
"supports": ["drafts", "revisions", "search"],
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true,
"searchable": true
},
{
"slug": "body",
"label": "Body",
"type": "portableText",
"searchable": true
}
]
}
],
"bylines": [
{
"id": "fixture-editorial",
"slug": "fixture-editorial",
"displayName": "Fixture Editorial"
}
]
}

View File

@@ -0,0 +1,40 @@
/**
* Minimal Astro config for Playwright e2e tests.
*
* Uses env vars for the database path and optional marketplace URL
* so each test run gets an isolated database.
*/
import node from "@astrojs/node";
import react from "@astrojs/react";
import { colorPlugin } from "@emdash-cms/plugin-color";
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
const dbUrl = process.env.EMDASH_TEST_DB || "file:./test.db";
const marketplaceUrl = process.env.EMDASH_MARKETPLACE_URL || undefined;
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
integrations: [
react(),
emdash({
database: sqlite({ url: dbUrl }),
plugins: [colorPlugin()],
marketplace: marketplaceUrl,
sandboxRunner: marketplaceUrl ? "./noop-sandbox.mjs" : undefined,
}),
],
i18n: {
defaultLocale: "en",
locales: ["en", "fr", "es"],
fallback: { fr: "en", es: "en" },
},
devToolbar: { enabled: false },
vite: {
server: {
fs: { strict: false },
},
},
});

40
e2e/fixture/emdash-env.d.ts vendored Normal file
View File

@@ -0,0 +1,40 @@
// Generated by EmDash on dev server start
// Do not edit manually
/// <reference types="emdash/locals" />
import type { ContentBylineCredit, PortableTextBlock } from "emdash";
export interface Page {
id: string;
slug: string | null;
status: string;
title: string;
body?: PortableTextBlock[];
createdAt: Date;
updatedAt: Date;
publishedAt: Date | null;
bylines?: ContentBylineCredit[];
}
export interface Post {
id: string;
slug: string | null;
status: string;
title: string;
featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number };
body?: PortableTextBlock[];
excerpt?: string;
theme_color?: string;
createdAt: Date;
updatedAt: Date;
publishedAt: Date | null;
bylines?: ContentBylineCredit[];
}
declare module "emdash" {
interface EmDashCollections {
pages: Page;
posts: Post;
}
}

View File

@@ -0,0 +1,10 @@
/**
* Noop sandbox runner for e2e tests.
*
* The marketplace admin pages only need `marketplace: true` in the manifest
* to render browse/detail UI. The sandbox runner is only used at install time.
* This stub satisfies the config validation without importing cloudflare:workers.
*/
import { createNoopSandboxRunner } from "emdash";
export { createNoopSandboxRunner as createSandboxRunner };

16
e2e/fixture/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "emdash-e2e-fixture",
"private": true,
"type": "module",
"dependencies": {
"@astrojs/node": "catalog:",
"@astrojs/react": "catalog:",
"@emdash-cms/auth": "workspace:*",
"@emdash-cms/plugin-color": "workspace:*",
"astro": "catalog:",
"better-sqlite3": "catalog:",
"emdash": "workspace:*",
"react": "catalog:",
"react-dom": "catalog:"
}
}

View File

@@ -0,0 +1,6 @@
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};

View File

@@ -0,0 +1,21 @@
---
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts");
---
<html>
<body>
<h1>Posts</h1>
<ul id="post-list">
{
posts.map((p) => (
<li>
<a href={`/posts/${p.id}`}>{p.data.title}</a>
{p.data.excerpt && <span class="excerpt">{p.data.excerpt}</span>}
</li>
))
}
</ul>
{posts.length === 0 && <p id="empty">No posts</p>}
</body>
</html>

View File

@@ -0,0 +1,21 @@
---
import { getEmDashEntry } from "emdash";
import { PortableText, Comments, CommentForm } from "emdash/ui";
const { slug } = Astro.params;
if (!slug) return Astro.redirect("/404");
const { entry: post } = await getEmDashEntry("posts", slug);
if (!post) return new Response("Not found", { status: 404 });
---
<html>
<body>
<article>
<h1 id="title">{post.data.title}</h1>
{post.data.excerpt && <p id="excerpt">{post.data.excerpt}</p>}
<div id="body"><PortableText value={post.data.body} /></div>
</article>
<Comments collection="posts" contentId={post.data.id} threaded />
<CommentForm collection="posts" contentId={post.data.id} />
</body>
</html>