merge: retire Canvas inspiration entry

# Conflicts:
#	README.md
#	src/components/layout/MainLayout.tsx
#	src/components/layout/Sidebar.tsx
#	tests/e2e/image-workspace-v2.spec.ts
#	tests/unit/app-module-provider-gate.test.tsx
#	tests/unit/title-bar.test.tsx
This commit is contained in:
inman
2026-09-07 10:24:24 +08:00
12 changed files with 128 additions and 114 deletions

View File

@@ -0,0 +1,102 @@
# Task: Remove Canvas inspiration entry
## Identity
- Task ID: 20260906-remove-canvas-inspiration-56e64d28
- Mode: Feature
- Branch: codex/20260906-remove-canvas-inspiration-56e64d28-remove-canvas-inspiration
- Worktree: /Users/inmanx/Documents/makelore-task-remove-canvas-inspiration-56e64d28
- Base commit: f8eee430f4f4167dc2ecc682ece96c652cdde14b
- Owner: codex
- Status: Ready for integration
## Scope
- Remove the user-visible “获取灵感” action from both expanded and collapsed
Canvas sidebars.
- Retire the Prompt Museum Renderer route and redirect historical
`/image-prompts` deep links to the Canvas workspace.
- Remove route-specific title-bar/sidebar treatment and update focused unit,
Electron E2E, README, and retained server-contract wording.
## Intent And Constraints
- Keep Canvas conversation, Living Form, Workspace, Quote, Task, Asset, project
creation, and project deletion behavior unchanged.
- Preserve Main-owned Prompt Museum API/auth/media security boundaries in this
small UI-removal task; do not infer authority to delete server data or change
backend contracts.
- Keep old links safe and deterministic by returning users to `/image-canvas`
without initializing Programming providers.
- Maintain the Chinese-only Makelore light interface and existing module-access
policy.
## Outcome
- Removed the lightbulb/inspiration action from both expanded and collapsed
Canvas sidebars.
- Retired the Prompt Museum Renderer route and its lazy bundle entry. Historical
`/image-prompts` paths now redirect to `/image-canvas` while remaining covered
by the Canvas module-access gate.
- Removed the obsolete Prompt Museum title-bar and sidebar-spacing special cases,
so Canvas keeps one workspace shell.
- Preserved the dormant Main-owned Prompt Museum Host API, authentication, media
validation, shared DTOs, and their existing focused tests; this task changes
only the AI Design product surface and does not delete server data.
- Updated README, retained server-contract wording, focused unit coverage, and
the Electron E2E flow to describe and verify the retired entry.
## Verification
- `corepack pnpm install --frozen-lockfile` — passed with repository-pinned pnpm
10.33.4.
- Focused Vitest coverage for App routing, Main layout, TitleBar, and the light
visual system — 4 files, 41 tests passed.
- `corepack pnpm run typecheck` — passed.
- `corepack pnpm run lint:check` — passed with zero errors and five pre-existing
warnings in unrelated Home/Makelore files.
- `corepack pnpm run build:vite` — passed for Renderer, Main, Preload, and the
release utility worker.
- `node ./node_modules/@playwright/test/cli.js test tests/e2e/image-workspace-v2.spec.ts --grep "removes the inspiration entry"`
— passed 1/1, confirming the sidebar entry is absent and the historical route
returns to Canvas.
- `corepack pnpm test` — passed: 1,859 ordinary tests passed, three skipped, and
the isolated pressure test passed 1/1.
- `git diff --check` — passed.
## Follow-ups
- A serialized Integration task should reconcile canonical Prompt Museum product
statements and split the mixed Museum/Canvas production-smoke commitment so
the still-relevant Canvas deletion and re-Quote checks remain tracked.
- Physically deleting the retained Main API, shared DTOs, or upstream data would
require a separate explicit scope; this task intentionally leaves those secure
boundaries intact and unreachable from the current product UI.
## Promotion Candidates
- Target: `.project-docs/30-worklog/current-state.md`,
`.project-docs/20-architecture/system-overview.md`,
`.project-docs/20-architecture/module-map.md`,
`.project-docs/20-architecture/data-flow.md`,
`.project-docs/40-domain/business-rules.md`,
`.project-docs/40-domain/glossary.md`, and
`.project-docs/80-commitments/commitments.md`.
- Proposal: record that Prompt Museum is no longer an active Canvas product
surface: expanded/collapsed sidebars expose no inspiration action, the page is
not mounted or bundled by App routing, and historical `/image-prompts` links
redirect to `/image-canvas`. Retained Main API/auth/media validation and shared
DTO code is dormant compatibility/security infrastructure rather than a visible
module. Remove Museum-specific production-smoke work from the mixed commitment
while retaining its Canvas deletion and re-Quote portions.
- Evidence: the user's explicit direction, focused 41-test regression suite,
full 1,859-test plus pressure pass, typecheck, lint, production build, and the
passing Electron route/sidebar check.
- Future impact: navigation and product documentation must not restore the
inspiration action or Prompt Museum route without a new explicit product
decision. Dormant API removal remains separately scoped.
- Semantic conflict: this supersedes canonical statements that Canvas exposes a
Prompt Museum entry and that its pending Prompt participates in active Canvas
data flow; it does not change ADR-007 or Main-owned security boundaries.
- Human confirmation: already supplied by the user in this task; no additional
product-direction approval is required during integration.

View File

@@ -5,7 +5,7 @@
Makelore 是一个面向软件、视觉创作与智能机器人的 AI 桌面工作台。当前版本为 `2.0.0`,包含三个已开通产品模块。模块入口页采用统一的横向卡片视觉,工作区左上角入口点击后返回模块入口页:
- `Makelore CodeAI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。
- `Makelore CanvasAI 绘画`每个设计项目Workspace维护一份从创建起就存在的 Living Form。用户先在中央对话时间线中让 AI 帮助描绘想法,再直接编辑同一时间线内的制作方案;右侧全高作品栏负责新建、切换和管理 Workspace,并提供“获取灵感”入口。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。
- `Makelore CanvasAI 绘画`每个设计项目Workspace维护一份从创建起就存在的 Living Form。用户先在中央对话时间线中让 AI 帮助描绘想法,再直接编辑同一时间线内的制作方案;右侧全高作品栏负责新建、切换和管理 Workspace。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。
- `Makelore RobotAI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。
应用启动默认进入 AI 模块入口选择页。入口页可在未登录状态浏览;未登录用户点击已开通模块时进入客户端原生登录页,可使用账号密码或手机号短信验证码登录。密码登录可选“记住密码”:正式安装包仅由 Electron Main 使用系统受保护凭据存储加密保存和回填账号密码,不写入 Renderer 持久状态,未打包开发版或系统安全存储不可用时禁用该选项。登录请求由 Renderer 经 Host API 交给 Electron Main再由 Main 调用 Works Square成功后回到入口选择页。已登录时Electron Main 会从 Works Square `/api/auth/me` 读取当前账号,只向 Renderer 投影用户名、账号/租户/部门标识、权限名列表与三个模块布尔开关,不透传上游资料或凭据。工作区门禁同时要求有效 Token 和完整用户身份;旧状态缺失身份时会先尝试从 Main 恢复,仍无法确认则清除残留会话并返回登录页。被管理员关闭的模块会在入口页置灰且无法点击,直接访问其工作区路径也会返回入口页。旧服务端未返回策略或缺少单项字段时默认开放;这个客户端门禁不替代服务端 API 授权。
@@ -33,7 +33,7 @@ Makelore 是一个面向软件、视觉创作与智能机器人的 AI 桌面工
- 官方插件运行架构Renderer 只调用 Main-owned Marketplace facadeMain 负责账号、请求 deadline、签名/摘要校验、不可变 Release、current selection 与原子回滚。Pi parent worker 使用冻结的 effective snapshot将每个有效 Skill 与已验证 Package Store root 成对传给 resource loader、Extension Host 和 CLI普通 `skill_only` 下载包不依赖运行时 Policy也不得携带可执行脚本。Project Scaffold 的 `.mjs` 是代码所有的 bundled 例外,只能从签名客户端固定资源根通过应用自带 Node 执行,不能扩展为 Marketplace 下载 artifact 的脚本权限。正式激活仍等待官方 Ed25519 公钥production key activation HOLD生产私钥只能来自部署 secret测试使用注入的临时密钥。
- 原生 Web Search 是冻结 selected model/provider/credential 的 parent-only 模型工具;只有 Works `model_capabilities` 明确声明支持时才出现,使用当前模型的正常计费链,不依赖 Marketplace 获取、Release、Admission、Hosted Provider 或 `agent_browser` 回退。
- 本机 Device Packages 只由 Conversation 工具发起安装,支持 npm、Git、绝对本地 Plugin 目录和 loose `SKILL.md`。Main 在预览和独立确认后提交不可变本机 generation禁用生命周期脚本确认界面明确提示 Pi extension 以桌面当前用户权限执行。新建/空闲 parent 会同时加载所有显式安装且当前启用的 Skill以及必需 Makelore 扩展之外的全部 Pi extensionactive parent 在 turn settled 后切换child 不继承这些资源,也不会扫描 ambient 项目或用户目录Renderer 不提供安装源选择器。
- 提示词博物馆只陈列经过审核的作品预览、Prompt、分类以及作者/来源/许可证信息,支持搜索、使用场景/风格/主体筛选和详情抽屉;“使用此 Prompt”只把原文带回当前 Canvas 对话草稿,不自动发送、不构成社区。列表和详情数据由服务端提供,客户端不打包数据集;服务端字段契约见 [`docs/prompt-museum-server-contract.md`](docs/prompt-museum-server-contract.md)。
- 提示词博物馆已退出当前 Canvas 产品面:右侧作品栏不再提供“获取灵感”,历史 `/image-prompts` 地址统一返回 Canvas。Main-owned API、媒体校验和共享 DTO 暂作为不可见兼容/安全基础设施保留;服务端边界见 [`docs/prompt-museum-server-contract.md`](docs/prompt-museum-server-contract.md)。
- 视觉系统:单一浅色主题,品牌蓝 `#3A5578`、星火橙 `#F26A3D`、白色画布与低饱和蓝灰层级。
- 字体系统Renderer UI 内嵌 Inter Variable 与经过字符子集化的 Source Han Sans SC WOFF2按字符范围统一中英文并保留系统中文字体 fallback代码、路径和日志使用独立等宽字体。
- 界面语言:仅保留中文;系统语言和历史设置中的其他语言会自动归一为中文。
@@ -99,7 +99,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p
- AI 编程项目配置只以项目内 `.makelore/project.json` 为准项目文件和会话主数据保持本地问答观察快照按个人资料同步规则单向上行。Main 不探测、读取或迁移 `.niancode``.opencode` 项目数据。
- AI 绘画 Renderer 只调用 Main-owned Host APIMain 负责 Works Square Token 刷新、Workspace 所属的持久 Agent Session、稳定命令身份、有界 Run 查询、可恢复事件订阅与契约映射,并通过本机 Host API 的 SSE 投影同步表单、任务和资产状态。切换 Workspace 只重连对应流;注销或退出时关闭本地流,不删除服务端持久 Session。远端 Token、Provider Prompt 与存储地址不进入 Renderer。
- AI 绘画使用独立的云端 Workspace 边界,不回退到 AI 编程项目数据,也不向 Renderer 暴露 Provider、模型、Prompt、存储 URI 或远端登录 Token。
- Prompt Museum 使用独立的 Main-owned Host API 代理Renderer 只接收分页卡片、详情和服务端返回的图片地址Works Square Token 只由 Main 持有。发布记录必须由服务端完成作者、来源、许可证和素材授权审核,模块不提供投稿、点赞、评论或排行榜
- 当前产品不挂载或打包 Prompt Museum Renderer 页面;历史路由只重定向到 Canvas。保留的 Main-owned Host API、媒体校验和共享 DTO 仍不得向 Renderer 暴露 Works Square Token且不代表可见产品入口或已部署内容服务
- AI 编程的智能体配置属于当前项目;稳定 id 用于保持对话兼容显示名称可以修改。AI 绘画的设计 Agent 是固定产品能力,不作为用户可增删的项目实体。
### 共享开发浏览器

View File

@@ -20,7 +20,7 @@ V1 不做:
- 在客户端打包静态 Prompt 数据集;
- 服务端代替用户自动发送 Prompt 或直接生成作品。
客户端入口位于 Canvas 侧栏“新建设计项目”上方,按钮文案为“获取灵感”
当前客户端不再提供 Prompt Museum 的可见入口;历史 `/image-prompts` 地址会重定向到 Canvas 主页面。以下合同仅记录仍由 Main 持有的服务端边界
## 2. 请求链路与接口

View File

@@ -39,7 +39,6 @@ const PreviewScene = lazy(() => import('./pages/Makelore').then(({ PreviewScene:
const ProjectConfiguration = lazy(() => import('./pages/ProjectConfiguration').then(({ ProjectConfiguration: component }) => ({ default: component })));
const Workbench = lazy(() => import('./pages/Workbench').then(({ Workbench: component }) => ({ default: component })));
const ImageCanvas = lazy(() => import('./pages/ImageCanvas').then(({ ImageCanvas: component }) => ({ default: component })));
const ImagePromptMuseum = lazy(() => import('./pages/ImagePromptMuseum').then(({ ImagePromptMuseum: component }) => ({ default: component })));
const AiHardware = lazy(() => import('./pages/AiHardware').then(({ AiHardware: component }) => ({ default: component })));
const Settings = lazy(() => import('./pages/Settings').then(({ Settings: component }) => ({ default: component })));
const Setup = lazy(() => import('./pages/Setup').then(({ Setup: component }) => ({ default: component })));
@@ -456,7 +455,7 @@ function App() {
<Route path="/chat" element={<ProjectChatRoute />} />
<Route path="/deliverables" element={<PreviewScene />} />
<Route path="/image-canvas" element={<ImageCanvas />} />
<Route path="/image-prompts" element={<ImagePromptMuseum />} />
<Route path="/image-prompts/*" element={<Navigate to="/image-canvas" replace />} />
<Route path="/ai-hardware" element={<AiHardware />} />
<Route path="/workbench/:projectId" element={<Workbench />} />
<Route path="/models" element={<Models />} />

View File

@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import {
FolderKanban,
Lightbulb,
Loader2,
Pencil,
Plus,
@@ -157,15 +156,6 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
>
<Plus className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="获取设计灵感"
onClick={() => navigate('/image-prompts')}
>
<Lightbulb className="h-4 w-4" />
</Button>
</div>
);
}
@@ -181,14 +171,6 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
<Plus className="h-4 w-4 text-brand" />
</button>
<button
type="button"
className="motion-press flex min-h-9 w-full items-center gap-2 rounded-xl px-2.5 text-left text-sm text-muted-foreground transition-colors hover:bg-surface-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
onClick={() => navigate('/image-prompts')}
>
<Lightbulb className="h-4 w-4" />
</button>
</div>
<div className="mb-2 mt-5 flex items-center justify-between px-2">

View File

@@ -26,7 +26,6 @@ export function MainLayout() {
});
const activeModule = getAiModuleForPath(location.pathname);
const isPaintingModule = activeModule === 'painting';
const isPromptMuseum = location.pathname === '/image-prompts' || location.pathname.startsWith('/image-prompts/');
const isCanvasWorkspace = location.pathname === '/image-canvas';
const isChatWorkspace = location.pathname === '/chat';
@@ -79,9 +78,8 @@ export function MainLayout() {
<TitleBar
integrated
workspaceLayout={isChatWorkspace}
overlay={isPaintingModule && !isPromptMuseum}
overlay={isPaintingModule}
showSidebarControls={!isCanvasWorkspace}
pageTitle={isPromptMuseum ? '获取灵感' : undefined}
sidebarPeekOpen={sidebarPeekOpen}
onSidebarPeekChange={handleSidebarPeekChange}
/>

View File

@@ -181,7 +181,6 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
const isProgrammingModule = activeModule === 'programming';
const isPaintingModule = activeModule === 'painting';
const isRobotModule = activeModule === 'robot';
const isPromptMuseum = location.pathname === '/image-prompts' || location.pathname.startsWith('/image-prompts/');
const visibleProjects = projects;
const selectedProjectFolderName = getFolderName(newProjectSelectedPath);
const accountName = userProfile?.displayName?.trim() || getAuthUserDisplayName(authUser) || '未登录用户';
@@ -549,7 +548,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
<div
className={cn(
'bg-transparent px-3 text-foreground',
isPaintingModule && !isPromptMuseum && !sidebarCollapsed ? 'pb-2 pt-10' : 'py-2',
isPaintingModule && !sidebarCollapsed ? 'pb-2 pt-10' : 'py-2',
sidebarCollapsed && 'px-2',
)}
>

View File

@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import {
FolderKanban,
Lightbulb,
Loader2,
Pencil,
Plus,
@@ -277,19 +276,6 @@ export function DesignWorksRail({
)}
</div>
<footer className="shrink-0 border-t border-border/60 p-3">
<button
type="button"
className="flex h-9 w-full items-center gap-2 rounded-xl px-2.5 text-xs font-medium text-muted-foreground hover:bg-surface-subtle hover:text-foreground"
onClick={() => {
window.location.hash = '/image-prompts';
}}
>
<Lightbulb className="h-4 w-4" />
</button>
</footer>
<Dialog open={dialog !== null} onOpenChange={(open) => !open && !saving && setDialog(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>

View File

@@ -1,7 +1,7 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('AI Design V2 workspace', () => {
test('keeps Prompt Museum cards usable when relative media fails', async ({ launchElectronApp }) => {
test('removes the inspiration entry and redirects its retired route to Canvas', async ({ launchElectronApp }) => {
test.setTimeout(90_000);
const app = await launchElectronApp({ skipSetup: true });
@@ -10,29 +10,6 @@ test.describe('AI Design V2 workspace', () => {
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await app.evaluate(({ ipcMain }) => {
const image = {
url: '/api/image-prompt-museum/e2e-entry/media/thumbnail',
width: 1200,
height: 900,
alt: 'E2E museum thumbnail',
};
const card = {
id: 'e2e-entry',
slug: 'e2e-entry',
title: '相对媒体 E2E',
summary: '验证图片失败不会阻断卡片。',
thumbnail: image,
categories: [],
model: { id: 'e2e-model', name: 'E2E Model' },
language: 'zh-CN',
attribution: {
author: { name: 'E2E 作者' },
source: { name: 'E2E 来源' },
license: { name: '测试许可', attributionText: 'E2E attribution' },
},
publishedAt: '2026-08-18T00:00:00.000Z',
updatedAt: '2026-08-18T00:00:00.000Z',
};
const respond = (json: unknown) => ({
ok: true,
data: { status: 200, ok: true, json },
@@ -50,47 +27,20 @@ test.describe('AI Design V2 workspace', () => {
},
});
}
if (path === '/api/works/image-prompt-museum') {
return respond({
success: true,
data: {
items: [card],
facets: { useCases: [], styles: [], subjects: [] },
nextCursor: null,
total: 1,
},
});
}
if (path === '/api/works/image-prompt-museum/e2e-entry') {
return respond({
success: true,
data: {
...card,
prompt: 'Create a resilient image card.',
variables: [],
images: [image],
requiresReferenceImages: false,
},
});
}
if (path === '/api/works/image-prompt-museum/e2e-entry/media/thumbnail') {
return respond({ mimeType: 'text/html', dataBase64: 'PGgxPnVuc2FmZTwvaDE+' });
}
return { ok: false, error: { message: `Unexpected E2E Host API request: ${path}` } };
});
});
await page.getByTestId('ai-module-option-painting').click();
const worksRail = page.getByTestId('image-workspace-works-rail');
await expect(worksRail.getByRole('button', { name: '获取灵感' })).toBeVisible();
await worksRail.getByRole('button', { name: '获取灵感' }).click();
await expect(worksRail).toBeVisible();
await expect(worksRail.getByRole('button', { name: /.*/u })).toHaveCount(0);
const cardButton = page.getByRole('button', { name: '查看 相对媒体 E2E' });
await expect(cardButton).toBeVisible();
await expect(page.getByLabel('E2E museum thumbnail加载失败')).toBeVisible();
await expect(cardButton).toBeEnabled();
await cardButton.click();
await expect(page.getByRole('button', { name: '关闭详情' })).toBeVisible();
await page.evaluate(() => {
window.location.hash = '#/image-prompts';
});
await expect.poll(() => page.evaluate(() => window.location.hash)).toBe('#/image-canvas');
await expect(page.getByTestId('image-workspace-works-rail')).toBeVisible();
} finally {
await closeElectronApp(app);
}

View File

@@ -123,6 +123,14 @@ describe('App programming provider initialization gate', () => {
expect(screen.queryByText('项目尚未初始化')).not.toBeInTheDocument();
});
it('redirects the retired inspiration route to the Canvas workspace', async () => {
await renderAt('/image-prompts/example');
expect(await screen.findByText('Canvas workspace')).toBeInTheDocument();
expect(screen.queryByText('Module chooser')).not.toBeInTheDocument();
expect(initProviders).not.toHaveBeenCalled();
});
it('redirects an orphaned token without a user identity away from a direct workspace route', async () => {
useAuthStore.setState({
initialized: true,

View File

@@ -64,16 +64,6 @@ describe('MainLayout module isolation', () => {
expect(screen.queryByTestId('sidebar-stub')).not.toBeInTheDocument();
});
it('keeps the user sidebar preference while moving the inspiration title into the title bar', () => {
renderLayout('/image-prompts');
expect(screen.getByTestId('titlebar-stub')).toHaveAttribute('data-page-title', '获取灵感');
expect(screen.getByTestId('titlebar-stub')).toHaveAttribute('data-overlay', 'false');
expect(screen.getByTestId('titlebar-stub')).toHaveAttribute('data-sidebar-collapsed', 'unset');
expect(screen.getByTestId('sidebar-stub')).toHaveAttribute('data-sidebar-collapsed', 'unset');
expect(useSettingsStore.getState().sidebarCollapsed).toBe(false);
});
it('does not overlay the local project initialization gate on AI hardware', () => {
renderLayout('/ai-hardware');

View File

@@ -132,13 +132,13 @@ describe('TitleBar platform behavior', () => {
expect(screen.getByTestId('titlebar-logo')).toBeInTheDocument();
});
it('keeps the painting page title after the macOS traffic-light safe area', () => {
it('keeps an optional page title after the macOS traffic-light safe area', () => {
window.electron.platform = 'darwin';
useSettingsStore.setState({ sidebarCollapsed: true });
render(<TitleBar integrated pageTitle="获取灵感" />);
render(<TitleBar integrated pageTitle="页面标题" />);
expect(screen.getByTestId('titlebar-page-title')).toHaveTextContent('获取灵感');
expect(screen.getByTestId('titlebar-page-title')).toHaveTextContent('页面标题');
expect(screen.getByTestId('titlebar-sidebar-surface')).toHaveClass('w-[132px]');
expect(screen.queryByTestId('titlebar-project-context')).not.toBeInTheDocument();
expect(screen.getByTestId('titlebar-logo').querySelectorAll('img')).toHaveLength(1);