Compare commits

..
Author SHA1 Message Date
dayuan.jiang 91b8d781bd ci: six e2e shards
With four, the shard of the long workspace tests took 8 minutes while
the others took 4 to 5.
2026-10-08 18:27:59 +09:00
dayuan.jiang 74a40082f4 ci: run the e2e tests in four shards
One job ran all 300+ tests with one worker, about 20 minutes. Four
jobs at once take a quarter each, still with one worker per job.
2026-10-08 18:11:46 +09:00
12 changed files with 93 additions and 161 deletions
+5 -5
View File
@@ -1,10 +1,10 @@
import { import {
ChevronDown, ChevronDown,
Download, Download,
FilePlus,
MessageSquareDashed, MessageSquareDashed,
PanelRightClose, PanelRightClose,
Settings, Settings2,
SquarePen,
} from "lucide-react" } from "lucide-react"
import { useRef } from "react" import { useRef } from "react"
import { DevXmlSimulator } from "@/components/dev-xml-simulator" import { DevXmlSimulator } from "@/components/dev-xml-simulator"
@@ -139,14 +139,14 @@ export function ChatPanel({
disabled={engine.isBusy} disabled={engine.isBusy}
data-testid="new-chat-button" data-testid="new-chat-button"
> >
<FilePlus /> <SquarePen />
</IconButton> </IconButton>
<IconButton <IconButton
label={dict.nav.settings} label={dict.nav.settings}
onClick={() => openSettings()} onClick={() => openSettings()}
data-testid="settings-button" data-testid="settings-button"
> >
<Settings /> <Settings2 />
</IconButton> </IconButton>
{!mobile && ( {!mobile && (
<IconButton <IconButton
@@ -173,7 +173,7 @@ export function ChatPanel({
/> />
)} )}
<div className="shrink-0 px-4 pt-1 pb-3"> <div className="shrink-0 p-2.5 pt-1">
<Composer <Composer
placeholder={ placeholder={
engine.messages.length > 0 && engine.messages.length > 0 &&
+42 -32
View File
@@ -18,18 +18,14 @@ interface Example {
file?: { path: string; name: string; type: string } file?: { path: string; name: string; type: string }
} }
/** Secondary start buttons: outlined, so they read as clickable */
const startButtonClass =
"inline-flex h-8 items-center gap-1.5 rounded-lg border border-border bg-card px-3 text-[13px] text-foreground transition-colors hover:bg-accent [&_svg]:text-muted-foreground"
async function loadExampleFile(file: NonNullable<Example["file"]>) { async function loadExampleFile(file: NonNullable<Example["file"]>) {
const response = await fetch(getAssetUrl(file.path)) const response = await fetch(getAssetUrl(file.path))
const blob = await response.blob() const blob = await response.blob()
return new File([blob], file.name, { type: file.type }) return new File([blob], file.name, { type: file.type })
} }
/** One start-screen example: a small picture over its title */ /** One start-screen choice: a small picture, a title and one line */
function ExampleTile({ function ExampleCard({
onClick, onClick,
title, title,
description, description,
@@ -39,7 +35,6 @@ function ExampleTile({
}: { }: {
onClick: () => void onClick: () => void
title: string title: string
/** Shown as the tooltip: the title alone names the example */
description: string description: string
picture: React.ReactNode picture: React.ReactNode
/** An icon on a tinted tile instead of a diagram on paper */ /** An icon on a tinted tile instead of a diagram on paper */
@@ -50,20 +45,24 @@ function ExampleTile({
<button <button
type="button" type="button"
onClick={onClick} onClick={onClick}
title={description} className="group/example flex min-w-0 items-center gap-2.5 rounded-xl border border-border bg-card p-1.5 pr-2.5 text-left transition-colors hover:border-foreground/25"
className="group/example flex min-w-0 flex-col gap-2 rounded-xl border border-border bg-card p-1.5 pb-2.5 text-left transition-colors hover:border-foreground/25"
data-testid={testId} data-testid={testId}
> >
<span <span
className={cn( className={cn(
"flex h-16 w-full items-center justify-center overflow-hidden rounded-lg", "flex h-12 w-16 shrink-0 items-center justify-center overflow-hidden rounded-lg",
plain ? "bg-muted" : "sheet-light bg-white", plain ? "bg-muted" : "sheet-light bg-white",
)} )}
> >
{picture} {picture}
</span> </span>
<span className="px-1 text-[13px] leading-snug font-medium text-foreground"> <span className="min-w-0 flex-1">
{title} <span className="line-clamp-2 text-[13px] leading-snug font-medium text-foreground">
{title}
</span>
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
{description}
</span>
</span> </span>
</button> </button>
) )
@@ -93,7 +92,6 @@ export function LobbyHero({
key: "paper", key: "paper",
title: dict.examples.paperToDiagram, title: dict.examples.paperToDiagram,
description: dict.examples.paperDescription, description: dict.examples.paperDescription,
image: "/paper_demo.svg",
prompt: "Summarize this paper as a diagram", prompt: "Summarize this paper as a diagram",
file: { file: {
path: "/chain-of-thought.txt", path: "/chain-of-thought.txt",
@@ -187,12 +185,10 @@ export function LobbyHero({
placeholder={t.placeholder} placeholder={t.placeholder}
/> />
{/* Examples: one row of tiles; the ways to bring your own
diagram follow as a separate group */}
<div <div
className={cn( className={cn(
"mt-5 grid gap-2", "mt-5 grid gap-2",
compact ? "grid-cols-1" : "grid-cols-5", compact ? "grid-cols-1" : "grid-cols-3",
)} )}
> >
{examples.map((example) => {examples.map((example) =>
@@ -213,7 +209,7 @@ export function LobbyHero({
</span> </span>
</button> </button>
) : ( ) : (
<ExampleTile <ExampleCard
key={example.key} key={example.key}
onClick={() => pickExample(example)} onClick={() => pickExample(example)}
title={example.title} title={example.title}
@@ -225,7 +221,7 @@ export function LobbyHero({
<img <img
src={getAssetUrl(example.image)} src={getAssetUrl(example.image)}
alt="" alt=""
className="max-h-14 max-w-[88%] object-contain transition-transform duration-300 group-hover/example:scale-[1.06]" className="max-h-10 max-w-[56px] object-contain transition-transform duration-300 group-hover/example:scale-[1.06]"
/> />
) : ( ) : (
<FileText className="size-5 text-muted-foreground" /> <FileText className="size-5 text-muted-foreground" />
@@ -235,34 +231,48 @@ export function LobbyHero({
/> />
), ),
)} )}
{!compact && (
<ExampleCard
onClick={onOpenFile}
title={dict.workspace.openFile}
description={t.openFileHint}
picture={
<FileUp className="size-5 text-muted-foreground" />
}
plain
/>
)}
</div> </div>
<p className="mt-2.5 text-center text-xs text-faint"> <p className="mt-3 text-center text-xs text-faint">
{dict.examples.cachedNote} {dict.examples.cachedNote}
</p> </p>
{/* Ways to start without an example, as plain buttons */} <div className="mt-6 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-[13px] text-muted-foreground">
<div className="mt-6 flex flex-wrap items-center justify-center gap-2">
{engine.sessions.length > 0 && ( {engine.sessions.length > 0 && (
<SessionMenu align="center" onOpenFile={onOpenFile}> <SessionMenu align="center" onOpenFile={onOpenFile}>
<button type="button" className={startButtonClass}> <button
type="button"
className="inline-flex items-center gap-1.5 hover:text-foreground"
>
<History className="size-4" /> <History className="size-4" />
{t.recent} {t.recent}
</button> </button>
</SessionMenu> </SessionMenu>
)} )}
<button {compact && (
type="button" <button
onClick={onOpenFile} type="button"
className={startButtonClass} onClick={onOpenFile}
data-testid="open-file" className="inline-flex items-center gap-1.5 hover:text-foreground"
> >
<FileUp className="size-4" /> <FileUp className="size-4" />
{dict.workspace.openFile} {dict.workspace.openFile}
</button> </button>
)}
<button <button
type="button" type="button"
onClick={onDrawYourself} onClick={onDrawYourself}
className={startButtonClass} className="inline-flex items-center gap-1.5 hover:text-foreground"
data-testid="draw-yourself" data-testid="draw-yourself"
> >
<Pencil className="size-4" /> <Pencil className="size-4" />
+26 -26
View File
@@ -2,6 +2,7 @@ import {
AlertCircle, AlertCircle,
Check, Check,
ChevronDown, ChevronDown,
Code2,
Copy, Copy,
Library, Library,
PenTool, PenTool,
@@ -179,23 +180,17 @@ function VersionCard({
</button> </button>
) )
// What the AI wrote: a text link in the summary line, so the card's // What the AI wrote, on every card (main showed it on every tool call)
// only button is the action (undo or restore) const codeButton = (
const summaryLine = ( <button
<div className="text-xs text-muted-foreground"> type="button"
{summary} onClick={() => setShowCode((v) => !v)}
{/* The link wraps as one unit with its separator */} aria-label={showCode ? t.hideCode : t.showCode}
<span className="whitespace-nowrap"> title={showCode ? t.hideCode : t.showCode}
<span aria-hidden> · </span> className="inline-flex size-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground hover:bg-accent hover:text-foreground"
<button >
type="button" <Code2 className="size-4" />
onClick={() => setShowCode((v) => !v)} </button>
className="underline decoration-foreground/30 underline-offset-2 hover:text-foreground hover:decoration-foreground"
>
{showCode ? t.hideCode : t.showCode}
</button>
</span>
</div>
) )
const codePanel = showCode && ( const codePanel = showCode && (
<div className="relative border-t border-border bg-surface-1 px-3 py-2.5"> <div className="relative border-t border-border bg-surface-1 px-3 py-2.5">
@@ -236,12 +231,15 @@ function VersionCard({
/> />
</button> </button>
{badge} {badge}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1" title={summary}>
<div className="truncate text-[13px] text-foreground"> <div className="truncate text-[13px] text-foreground">
{title} {title}
</div> </div>
{summaryLine} <div className="truncate text-xs text-muted-foreground">
{summary}
</div>
</div> </div>
{codeButton}
{restoreButton} {restoreButton}
</div> </div>
{codePanel} {codePanel}
@@ -273,12 +271,17 @@ function VersionCard({
{t.compare} {t.compare}
</span> </span>
</button> </button>
{/* Title row with the action; the summary runs under both */} <div className="flex items-center gap-2.5 px-3 py-2.5">
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2.5 px-3 py-2.5">
{badge} {badge}
<div className="truncate text-[13px] font-medium text-foreground"> <div className="min-w-0 flex-1" title={summary}>
{title} <div className="truncate text-[13px] font-medium text-foreground">
{title}
</div>
<div className="line-clamp-2 text-xs text-muted-foreground">
{summary}
</div>
</div> </div>
{codeButton}
{canUndo ? ( {canUndo ? (
<button <button
type="button" type="button"
@@ -297,9 +300,6 @@ function VersionCard({
) : ( ) : (
restoreButton restoreButton
)} )}
<div className="col-span-2 col-start-2 mt-0.5">
{summaryLine}
</div>
</div> </div>
{codePanel} {codePanel}
</div> </div>
+2 -2
View File
@@ -220,14 +220,14 @@ export function ModelSelector({
<span className="min-w-0 truncate text-xs"> <span className="min-w-0 truncate text-xs">
{selectedModel {selectedModel
? shortModelName(selectedModel.modelId) ? shortModelName(selectedModel.modelId)
: dict.modelConfig.serverDefault} : dict.modelConfig.default}
</span> </span>
) : ( ) : (
// Keep an sr-only label for screen readers when hidden // Keep an sr-only label for screen readers when hidden
<span className="sr-only"> <span className="sr-only">
{selectedModel {selectedModel
? selectedModel.modelId ? selectedModel.modelId
: dict.modelConfig.serverDefault} : dict.modelConfig.default}
</span> </span>
)} )}
<ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" /> <ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
+2 -2
View File
@@ -1,6 +1,6 @@
"use client" "use client"
import { MessageSquare, PanelRightOpen, Settings, Shapes } from "lucide-react" import { MessageSquare, PanelRightOpen, Settings2, Shapes } from "lucide-react"
import { usePathname, useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import { CanvasStage } from "@/components/canvas/canvas-stage" import { CanvasStage } from "@/components/canvas/canvas-stage"
@@ -100,7 +100,7 @@ function StartBar({
onClick={() => openSettings()} onClick={() => openSettings()}
data-testid="settings-button" data-testid="settings-button"
> >
<Settings /> <Settings2 />
</IconButton> </IconButton>
</div> </div>
) )
+4 -4
View File
@@ -72,7 +72,7 @@
"replicateDescription": "Upload and replicate an existing flowchart", "replicateDescription": "Upload and replicate an existing flowchart",
"creativeDrawing": "Creative drawing", "creativeDrawing": "Creative drawing",
"creativeDescription": "Draw something fun and creative", "creativeDescription": "Draw something fun and creative",
"cachedNote": "Examples use saved answers: they appear instantly and don't call the AI.", "cachedNote": "Examples open instantly and don't use your model.",
"mcpServer": "MCP Server", "mcpServer": "MCP Server",
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor" "mcpDescription": "Use in Claude Desktop, VS Code & Cursor"
}, },
@@ -96,7 +96,7 @@
"language": "Language", "language": "Language",
"languageDescription": "Choose your interface language.", "languageDescription": "Choose your interface language.",
"theme": "Theme", "theme": "Theme",
"themeDescription": "Light or dark interface. The canvas follows the same theme.", "themeDescription": "Light or dark interface. The diagram always stays on a white sheet, the way it exports.",
"diagramStyle": "Diagram style", "diagramStyle": "Diagram style",
"diagramStyleDescription": "How the AI draws new diagrams: Styled uses colors and icons, Minimal keeps shapes plain.", "diagramStyleDescription": "How the AI draws new diagrams: Styled uses colors and icons, Minimal keeps shapes plain.",
"sendShortcut": "Send shortcut", "sendShortcut": "Send shortcut",
@@ -400,7 +400,7 @@
"searchModels": "Search models...", "searchModels": "Search models...",
"noModelsFound": "No models found.", "noModelsFound": "No models found.",
"default": "Default", "default": "Default",
"serverDefault": "Server model", "serverDefault": "Server Default",
"serverModels": "Server Models", "serverModels": "Server Models",
"userModels": "User Models", "userModels": "User Models",
"configureModels": "Configure Models...", "configureModels": "Configure Models...",
@@ -695,7 +695,7 @@
}, },
"lobby": { "lobby": {
"title": "What would you like to draw?", "title": "What would you like to draw?",
"lede": "Describe a diagram or upload a screenshot or PDF. You get a draw.io diagram you can keep editing and export.", "lede": "Describe a diagram, or upload a screenshot or PDF. You get a regular draw.io diagram that you can keep editing and export.",
"placeholder": "For example: a login sequence diagram with SMS codes and retries", "placeholder": "For example: a login sequence diagram with SMS codes and retries",
"recent": "Recent diagrams", "recent": "Recent diagrams",
"drawYourself": "Start with a blank canvas", "drawYourself": "Start with a blank canvas",
+4 -4
View File
@@ -72,7 +72,7 @@
"replicateDescription": "既存のフローチャートをアップロードして複製", "replicateDescription": "既存のフローチャートをアップロードして複製",
"creativeDrawing": "自由なイラスト", "creativeDrawing": "自由なイラスト",
"creativeDescription": "楽しくてクリエイティブなものを描く", "creativeDescription": "楽しくてクリエイティブなものを描く",
"cachedNote": "例は保存済みの回答を使うので、すぐに表示され、AI を呼び出しません。", "cachedNote": "例はすぐに開き、モデルは使いません。",
"mcpServer": "MCP サーバー", "mcpServer": "MCP サーバー",
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用" "mcpDescription": "Claude Desktop、VS Code、Cursor で使用"
}, },
@@ -96,7 +96,7 @@
"language": "言語", "language": "言語",
"languageDescription": "インターフェース言語を選択します。", "languageDescription": "インターフェース言語を選択します。",
"theme": "テーマ", "theme": "テーマ",
"themeDescription": "画面をライトにするかダークにするか。キャンバスも同じテーマになります。", "themeDescription": "画面をライトにするかダークにするか。図はエクスポートしたときと同じく、常に白い用紙の上に表示されます。",
"diagramStyle": "図のスタイル", "diagramStyle": "図のスタイル",
"diagramStyleDescription": "AI が新しい図を描くときのスタイル。スタイル付きは色やアイコンを使い、ミニマルは素朴な図形だけにします。", "diagramStyleDescription": "AI が新しい図を描くときのスタイル。スタイル付きは色やアイコンを使い、ミニマルは素朴な図形だけにします。",
"sendShortcut": "送信のショートカット", "sendShortcut": "送信のショートカット",
@@ -355,7 +355,7 @@
"searchModels": "モデルを検索...", "searchModels": "モデルを検索...",
"noModelsFound": "モデルが見つかりません。", "noModelsFound": "モデルが見つかりません。",
"default": "デフォルト", "default": "デフォルト",
"serverDefault": "サーバーのモデル", "serverDefault": "サーバーデフォルト",
"serverModels": "サーバーモデル", "serverModels": "サーバーモデル",
"userModels": "ユーザーモデル", "userModels": "ユーザーモデル",
"configureModels": "モデルを設定...", "configureModels": "モデルを設定...",
@@ -695,7 +695,7 @@
}, },
"lobby": { "lobby": {
"title": "どんな図を描きますか?", "title": "どんな図を描きますか?",
"lede": "図を説明するか、スクリーンショットや PDF をアップロードしてください。できあがるのは編集もエクスポートもできる draw.io の図です。", "lede": "図の内容を説明するか、スクリーンショットや PDF をアップロードしてください。できあがるのは通常の draw.io の図なので、続けて編集もエクスポートもできます。",
"placeholder": "例:SMS 認証とリトライを含むログインのシーケンス図", "placeholder": "例:SMS 認証とリトライを含むログインのシーケンス図",
"recent": "最近の図", "recent": "最近の図",
"drawYourself": "白紙のキャンバスから始める", "drawYourself": "白紙のキャンバスから始める",
+4 -4
View File
@@ -72,7 +72,7 @@
"replicateDescription": "上傳並複製現有流程圖", "replicateDescription": "上傳並複製現有流程圖",
"creativeDrawing": "創意繪畫", "creativeDrawing": "創意繪畫",
"creativeDescription": "繪製有趣且富有創意的內容", "creativeDescription": "繪製有趣且富有創意的內容",
"cachedNote": "範例使用保存好的答案,立即顯示,不呼叫 AI。", "cachedNote": "範例會立即開啟,不呼叫模型。",
"mcpServer": "MCP 伺服器", "mcpServer": "MCP 伺服器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用" "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
}, },
@@ -96,7 +96,7 @@
"language": "語言", "language": "語言",
"languageDescription": "選擇介面語言。", "languageDescription": "選擇介面語言。",
"theme": "主題", "theme": "主題",
"themeDescription": "介面用淺色還是深色,畫布跟隨同一主題。", "themeDescription": "介面用淺色還是深色。圖紙始終是白底,和匯出後的樣子一致。",
"diagramStyle": "圖形風格", "diagramStyle": "圖形風格",
"diagramStyleDescription": "AI 畫新圖時的風格:精緻會用顏色和圖示,簡約只用樸素的圖形。", "diagramStyleDescription": "AI 畫新圖時的風格:精緻會用顏色和圖示,簡約只用樸素的圖形。",
"sendShortcut": "傳送快速鍵", "sendShortcut": "傳送快速鍵",
@@ -400,7 +400,7 @@
"searchModels": "搜尋模型...", "searchModels": "搜尋模型...",
"noModelsFound": "未找到模型。", "noModelsFound": "未找到模型。",
"default": "預設", "default": "預設",
"serverDefault": "伺服器模型", "serverDefault": "伺服器預設",
"serverModels": "伺服器模型", "serverModels": "伺服器模型",
"userModels": "使用者模型", "userModels": "使用者模型",
"configureModels": "配置模型...", "configureModels": "配置模型...",
@@ -695,7 +695,7 @@
}, },
"lobby": { "lobby": {
"title": "說說你要畫什麼", "title": "說說你要畫什麼",
"lede": "描述一張圖,或上傳截圖、PDF。得到的是可以繼續編輯和匯出的 draw.io 圖。", "lede": "描述一張圖,或上傳截圖、PDF。畫出來的是標準的 draw.io 圖表,可以接著改,也可以匯出。",
"placeholder": "例如:使用者登入的時序圖,包含簡訊驗證碼和失敗重試", "placeholder": "例如:使用者登入的時序圖,包含簡訊驗證碼和失敗重試",
"recent": "最近的圖紙", "recent": "最近的圖紙",
"drawYourself": "從空白畫布開始", "drawYourself": "從空白畫布開始",
+4 -4
View File
@@ -72,7 +72,7 @@
"replicateDescription": "上传并复制现有流程图", "replicateDescription": "上传并复制现有流程图",
"creativeDrawing": "创意绘画", "creativeDrawing": "创意绘画",
"creativeDescription": "绘制有趣且富有创意的内容", "creativeDescription": "绘制有趣且富有创意的内容",
"cachedNote": "示例使用保存好的答案,立即显示,不调用 AI。", "cachedNote": "示例会立即打开,不调用模型。",
"mcpServer": "MCP 服务器", "mcpServer": "MCP 服务器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用" "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
}, },
@@ -96,7 +96,7 @@
"language": "语言", "language": "语言",
"languageDescription": "选择界面语言。", "languageDescription": "选择界面语言。",
"theme": "主题", "theme": "主题",
"themeDescription": "界面用浅色还是深色,画布跟随同一主题。", "themeDescription": "界面用浅色还是深色。图纸始终是白底,和导出后的样子一致。",
"diagramStyle": "图形风格", "diagramStyle": "图形风格",
"diagramStyleDescription": "AI 画新图时的风格:精致会用颜色和图标,简约只用朴素的图形。", "diagramStyleDescription": "AI 画新图时的风格:精致会用颜色和图标,简约只用朴素的图形。",
"sendShortcut": "发送快捷键", "sendShortcut": "发送快捷键",
@@ -400,7 +400,7 @@
"searchModels": "搜索模型...", "searchModels": "搜索模型...",
"noModelsFound": "未找到模型。", "noModelsFound": "未找到模型。",
"default": "默认", "default": "默认",
"serverDefault": "服务器模型", "serverDefault": "服务器默认",
"serverModels": "服务器模型", "serverModels": "服务器模型",
"userModels": "用户模型", "userModels": "用户模型",
"configureModels": "配置模型...", "configureModels": "配置模型...",
@@ -695,7 +695,7 @@
}, },
"lobby": { "lobby": {
"title": "说说你要画什么", "title": "说说你要画什么",
"lede": "描述一张图,或上传截图、PDF。得到的是可以继续编辑和导出的 draw.io 图。", "lede": "描述一张图,或者上传截图、PDF。画出来的是标准的 draw.io 图表,可以接着改,也可以导出。",
"placeholder": "比如:用户登录的时序图,包含短信验证码和失败重试", "placeholder": "比如:用户登录的时序图,包含短信验证码和失败重试",
"recent": "最近的图纸", "recent": "最近的图纸",
"drawYourself": "从空白画布开始", "drawYourself": "从空白画布开始",
@@ -216,20 +216,6 @@ export function applyDiagramOperations(
continue continue
} }
// A cut-off cell (its XML still streaming in a preview) would
// parse in Chrome to a cell without its geometry
const syntaxError = getXmlSyntaxError(
`<wrapper>${op.new_xml}</wrapper>`,
)
if (syntaxError) {
errors.push({
type: "update",
cellId: op.cell_id,
message: `new_xml is not well-formed XML: ${syntaxError}`,
})
continue
}
// Parse the new XML // Parse the new XML
const newDoc = parser.parseFromString( const newDoc = parser.parseFromString(
`<wrapper>${op.new_xml}</wrapper>`, `<wrapper>${op.new_xml}</wrapper>`,
@@ -282,20 +268,6 @@ export function applyDiagramOperations(
continue continue
} }
// A cut-off cell (its XML still streaming in a preview) would
// parse in Chrome to a cell without its geometry
const syntaxError = getXmlSyntaxError(
`<wrapper>${op.new_xml}</wrapper>`,
)
if (syntaxError) {
errors.push({
type: "add",
cellId: op.cell_id,
message: `new_xml is not well-formed XML: ${syntaxError}`,
})
continue
}
// Parse the new XML // Parse the new XML
const newDoc = parser.parseFromString( const newDoc = parser.parseFromString(
`<wrapper>${op.new_xml}</wrapper>`, `<wrapper>${op.new_xml}</wrapper>`,
@@ -149,52 +149,3 @@ describe("a wrapped mxCell with its wrapper's id", () => {
expect(result).toContain('label="B"') expect(result).toContain('label="B"')
}) })
}) })
describe("cut-off new_xml", () => {
// While edit_diagram streams, the last operation's XML is often
// incomplete. Chrome's DOMParser keeps the partial cell, so it must be
// refused before it reaches the diagram.
const CUT = `<mxCell id="b" value="B2" style="rounded=1;" vertex="1" parent="1">`
it("refuses an update whose XML is cut off", () => {
const { result, errors } = applyDiagramOperations(DOC, [
{ operation: "update", cell_id: "b", new_xml: CUT },
])
expect(errors).toHaveLength(1)
expect(errors[0]).toMatchObject({ type: "update", cellId: "b" })
expect(errors[0].message).toContain("not well-formed")
expect(result).toContain('value="B"')
expect(result).not.toContain("B2")
})
it("refuses an add whose XML is cut off", () => {
const { result, errors } = applyDiagramOperations(DOC, [
{
operation: "add",
cell_id: "n",
new_xml: `<mxCell id="n" value="N" vertex="1" parent="1"><mxGeometry x="1" y="2" width="3"`,
},
])
expect(errors).toHaveLength(1)
expect(errors[0]).toMatchObject({ type: "add", cellId: "n" })
expect(result).not.toContain('id="n"')
})
it("applies the complete operations before a cut-off one", () => {
const { result, errors } = applyDiagramOperations(DOC, [
{
operation: "update",
cell_id: "b",
new_xml: `<mxCell id="b" value="B3" vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell>`,
},
{
operation: "add",
cell_id: "n",
new_xml: `<mxCell id="n" value="N" vertex="1" parent="1">`,
},
])
expect(errors).toHaveLength(1)
expect(result).toContain('value="B3"')
expect(result).not.toContain('id="n"')
})
})
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 67 KiB