ソースを参照

test(editor): add visual regression suite (Phase 3), fix TS build errors

- Add 21 visual regression tests across blurred/focused states,
  block types, themes (default/dark/orange), marker modes
- Add `?theme=` and `?marker-mode=` query params to dev editor page
- Configure playwright screenshot options (maxDiffPixelRatio: 0.02)
- Fix TS errors from noUncheckedIndexedAccess:
  - Guard array access on blocks.value[idx] before spread
  - Fix update:content handler type (string | undefined)
- Generate 22 Chromium baseline snapshots
Zander Hawke 23 時間 前
コミット
aa0a64bd45
25 ファイル変更209 行追加3 行削除
  1. 14 1
      apps/dev/src/pages/editor.vue
  2. 184 0
      packages/editor/e2e/visual/regression.spec.ts
  3. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/bold-blurred-chromium-darwin.png
  4. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/bold-focused-chromium-darwin.png
  5. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/callout-blurred-chromium-darwin.png
  6. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/callout-focused-chromium-darwin.png
  7. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/code-block-chromium-darwin.png
  8. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/code-focused-chromium-darwin.png
  9. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/editor-after-typing-chromium-darwin.png
  10. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/editor-initial-chromium-darwin.png
  11. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/heading-blurred-chromium-darwin.png
  12. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/inline-formatting-blurred-chromium-darwin.png
  13. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/live-preview-blurred-chromium-darwin.png
  14. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/markers-always-visible-chromium-darwin.png
  15. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/page-ref-chips-chromium-darwin.png
  16. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/properties-chromium-darwin.png
  17. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/table-blurred-chromium-darwin.png
  18. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/table-focused-chromium-darwin.png
  19. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/tag-styling-chromium-darwin.png
  20. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/task-states-chromium-darwin.png
  21. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/theme-dark-chromium-darwin.png
  22. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/theme-default-chromium-darwin.png
  23. BIN
      packages/editor/e2e/visual/regression.spec.ts-snapshots/theme-orange-chromium-darwin.png
  24. 6 0
      packages/editor/playwright.config.ts
  25. 5 2
      packages/editor/src/components/Editor.vue

+ 14 - 1
apps/dev/src/pages/editor.vue

@@ -6,6 +6,18 @@ const isE2E = computed(() =>
   typeof window !== "undefined" &&
   new URLSearchParams(window.location.search).get("e2e") === "true",
 )
+const themeOverride = computed(() => {
+  if (typeof window === "undefined") return undefined
+  const t = new URLSearchParams(window.location.search).get("theme")
+  if (t === "dark" || t === "orange") return t
+  return undefined
+})
+const markerMode = computed(() => {
+  if (typeof window === "undefined") return "always-visible"
+  const m = new URLSearchParams(window.location.search).get("marker-mode")
+  if (m === "live-preview" || m === "always-visible") return m
+  return "always-visible"
+})
 
 const content = ref(`
 * # Welcome to Enesis Editor
@@ -92,7 +104,8 @@ function onTagClick({ tag }: { tag: string }) {
             <Editor
               v-model:content="content"
               :focused="true"
-              marker-mode="always-visible"
+              :marker-mode="markerMode"
+              :theme="themeOverride"
               :resolved-refs="resolvedRefs"
               :resolved-refs-version="resolvedRefs.size"
               :test-mode="isE2E"

+ 184 - 0
packages/editor/e2e/visual/regression.spec.ts

@@ -0,0 +1,184 @@
+import { expect, test } from "@playwright/test"
+
+test.describe("Visual regression @visual", () => {
+  test.beforeEach(async ({ page }) => {
+    await page.addStyleTag({
+      content: `
+        body {
+          -webkit-font-smoothing: antialiased;
+          -moz-osx-font-smoothing: grayscale;
+        }
+      `,
+    })
+  })
+
+  // ── Block-level blurred state screenshots ────────────────────────────
+
+  test("heading block blurred @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true")
+    const pm = page.locator('[role="textbox"][data-block-id]').first()
+    await pm.focus()
+    await page.keyboard.type("# Heading One")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("heading-blurred.png")
+  })
+
+  test("bold text blurred @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("bold-blurred.png")
+  })
+
+  test("inline formatting blurred @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    const blockIds = await page.evaluate(() => window.__testUtils__?.getBlockIds())
+    // Text formatting block (index 2): italic, code, strikethrough, highlight
+    await page.evaluate(
+      (bid: string) => window.__testUtils__?.focusAtEnd(bid),
+      blockIds![2],
+    )
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("inline-formatting-blurred.png")
+  })
+
+  test("page ref chip blurred @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("page-ref-chips.png")
+  })
+
+  test("tag styling blurred @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("tag-styling.png")
+  })
+
+  test("task states blurred @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("task-states.png")
+  })
+
+  test("table as HTML blurred @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("table-blurred.png")
+  })
+
+  test("code block rendered @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("code-block.png")
+  })
+
+  test("callout block blurred @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("callout-blurred.png")
+  })
+
+  test("properties rendered @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await expect(page).toHaveScreenshot("properties.png")
+  })
+
+  // ── Focused state screenshots ────────────────────────────────────────
+
+  test("bold markers visible focused @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true")
+    const pm = page.locator('[role="textbox"][data-block-id]').first()
+    await pm.focus()
+    await page.keyboard.type("**visible markers**")
+    await expect(page).toHaveScreenshot("bold-focused.png")
+  })
+
+  test("table raw pipes focused @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true")
+    const blockIds = await page.evaluate(() => window.__testUtils__?.getBlockIds())
+    // Table block is at index 6
+    await page.evaluate(
+      (bid: string) => window.__testUtils__?.focusAtEnd(bid),
+      blockIds![6],
+    )
+    await page.keyboard.type("| ")
+    await expect(page).toHaveScreenshot("table-focused.png")
+  })
+
+  test("code block focused @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true")
+    const blockIds = await page.evaluate(() => window.__testUtils__?.getBlockIds())
+    // Code block is at index 7
+    await page.evaluate(
+      (bid: string) => window.__testUtils__?.focusAtEnd(bid),
+      blockIds![7],
+    )
+    await expect(page).toHaveScreenshot("code-focused.png")
+  })
+
+  test("callout block focused @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true")
+    const blockIds = await page.evaluate(() => window.__testUtils__?.getBlockIds())
+    // Callout block is at index 8
+    await page.evaluate(
+      (bid: string) => window.__testUtils__?.focusAtEnd(bid),
+      blockIds![8],
+    )
+    await expect(page).toHaveScreenshot("callout-focused.png")
+  })
+
+  // ── Theme screenshots ────────────────────────────────────────────────
+
+  test("default theme @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&theme=default")
+    await page.waitForTimeout(300)
+    await expect(page).toHaveScreenshot("theme-default.png")
+  })
+
+  test("dark theme @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&theme=dark")
+    await page.waitForTimeout(300)
+    await expect(page).toHaveScreenshot("theme-dark.png")
+  })
+
+  test("orange monochrome theme @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&theme=orange")
+    await page.waitForTimeout(300)
+    await expect(page).toHaveScreenshot("theme-orange.png")
+  })
+
+  // ── Full editor state screenshots ────────────────────────────────────
+
+  test("full editor initial load @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true")
+    await page.waitForTimeout(300)
+    await expect(page).toHaveScreenshot("editor-initial.png")
+  })
+
+  test("full editor after typing @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true")
+    const blockIds = await page.evaluate(() => window.__testUtils__?.getBlockIds())
+    await page.evaluate(
+      (bid: string) => window.__testUtils__?.focusAtEnd(bid),
+      blockIds![blockIds!.length - 1],
+    )
+    await page.keyboard.type("\n\n# New Content\n\nTyped by the user.")
+    await page.waitForTimeout(300)
+    await expect(page).toHaveScreenshot("editor-after-typing.png")
+  })
+
+  // ── Marker mode comparison ───────────────────────────────────────────
+
+  test("always-visible mode shows markers @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=always-visible")
+    await page.waitForTimeout(300)
+    await expect(page).toHaveScreenshot("markers-always-visible.png")
+  })
+
+  test("live-preview mode hides markers on blur @visual", async ({ page }) => {
+    await page.goto("/editor?e2e=true&marker-mode=live-preview")
+    await page.evaluate(() => window.__testUtils__?.toggleBlur())
+    await page.waitForTimeout(100)
+    await expect(page).toHaveScreenshot("live-preview-blurred.png")
+  })
+})

BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/bold-blurred-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/bold-focused-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/callout-blurred-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/callout-focused-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/code-block-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/code-focused-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/editor-after-typing-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/editor-initial-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/heading-blurred-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/inline-formatting-blurred-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/live-preview-blurred-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/markers-always-visible-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/page-ref-chips-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/properties-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/table-blurred-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/table-focused-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/tag-styling-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/task-states-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/theme-dark-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/theme-default-chromium-darwin.png


BIN
packages/editor/e2e/visual/regression.spec.ts-snapshots/theme-orange-chromium-darwin.png


+ 6 - 0
packages/editor/playwright.config.ts

@@ -6,6 +6,12 @@ export default defineConfig({
   retries: 2,
   workers: process.env.CI ? 1 : undefined,
   reporter: [["html", { open: "never" }]],
+  outputDir: "test-results",
+  expect: {
+    toHaveScreenshot: {
+      maxDiffPixelRatio: 0.02,
+    },
+  },
   use: {
     baseURL: "http://localhost:5173",
     trace: "on-first-retry",

+ 5 - 2
packages/editor/src/components/Editor.vue

@@ -398,7 +398,10 @@ function onBlockContentChange(blockId?: string, newContent?: string) {
   if (blockId && newContent) {
     const idx = blocks.value.findIndex((b) => b.id === blockId)
     if (idx >= 0) {
-      blocks.value[idx] = { ...blocks.value[idx], content: newContent }
+      const b = blocks.value[idx]
+      if (b) {
+        blocks.value[idx] = { ...b, content: newContent }
+      }
     }
   }
   lastSerialized = serializeBlocks(blocks.value)
@@ -1003,7 +1006,7 @@ defineExpose({
           :resolved-refs-version="resolvedRefsVersion"
           :resolve-asset="resolveAsset"
           class="flex-1 min-w-0"
-          @update:content="(val: string) => onBlockContentChange(block.id, val)"
+          @update:content="(val: string | undefined) => onBlockContentChange(block.id, val)"
           @split="(before, after) => onSplit(i, before, after)"
           @merge-previous="onMergePrevious(i)"
           @delete-if-empty="onDeleteIfEmpty(i)"