mirror of
https://github.com/remorses/gpuix.git
synced 2026-10-29 03:04:52 +08:00
`/tmp` does not exist on Windows, and native `save()` never creates the
parent directory, so every capture in styles, events, img and the diff
example failed the whole test there with
Failed to save screenshot: The system cannot find the path
specified. (os error 3)
That is 34 of the 41 failures in the new Windows test job.
`SHOTS_DIR` already pointed at packages/react/screenshots for exactly
this reason, so the fix is to use it, and to create it where it is
defined rather than in a `beforeAll` each file had to remember. The
examples diff test gets the same treatment against
examples/screenshots.
Session: ses_fc0b7cd62ffe3aBw0pyYBOBDjf
360 lines
11 KiB
TypeScript
360 lines
11 KiB
TypeScript
/**
|
|
* Visual tests for the GPUIX diff viewer example.
|
|
*
|
|
* Uses the native GPUI test renderer (real Metal rendering on macOS) to
|
|
* validate diff rendering end-to-end: React → Rust RetainedTree → GpuixView →
|
|
* GPUI layout → Metal → screenshot.
|
|
*
|
|
* Each test renders a diff, takes a screenshot, and verifies text content.
|
|
* Screenshots are saved to `examples/screenshots/` for manual inspection.
|
|
*
|
|
* @ts-nocheck
|
|
*/
|
|
|
|
import fs from "fs"
|
|
import path from "path"
|
|
import { fileURLToPath } from "url"
|
|
import { describe, it, expect, beforeEach } from "vitest"
|
|
import React from "react"
|
|
import { createTestRoot, hasNativeTestRenderer, type TestRoot } from "@gpuix/react/testing"
|
|
import type { StructuredPatchHunk as Hunk } from "diff"
|
|
import { DiffViewer } from "./diff"
|
|
|
|
const describeNative = hasNativeTestRenderer ? describe : describe.skip
|
|
|
|
// Not `/tmp`: that path does not exist on Windows, and native never creates
|
|
// the parent directory, so every capture failed the test there.
|
|
const SCREENSHOT_DIR = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
"screenshots",
|
|
)
|
|
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true })
|
|
|
|
// Background for the scroll container — matches the diff viewer's unchanged line bg
|
|
// so the area below content doesn't look different.
|
|
const DIFF_BG = "rgba(15, 15, 15, 1)"
|
|
|
|
// ── Test fixtures ────────────────────────────────────────────────────
|
|
|
|
const simpleHunks: Hunk[] = [
|
|
{
|
|
oldStart: 1,
|
|
oldLines: 4,
|
|
newStart: 1,
|
|
newLines: 4,
|
|
lines: [
|
|
" const x = 1;",
|
|
"-const y = 2;",
|
|
"+const y = 3;",
|
|
" const z = x + y;",
|
|
],
|
|
},
|
|
]
|
|
|
|
const multiHunkPatch: Hunk[] = [
|
|
{
|
|
oldStart: 1,
|
|
oldLines: 5,
|
|
newStart: 1,
|
|
newLines: 7,
|
|
lines: [
|
|
" import React from 'react'",
|
|
" import { useState } from 'react'",
|
|
" ",
|
|
"-function Counter({ initial }: { initial: number }) {",
|
|
"- const [count, setCount] = useState(initial)",
|
|
"+interface CounterProps {",
|
|
"+ initial: number",
|
|
"+ step?: number",
|
|
"+}",
|
|
"+",
|
|
"+function Counter({ initial, step = 1 }: CounterProps) {",
|
|
"+ const [count, setCount] = useState(initial)",
|
|
],
|
|
},
|
|
{
|
|
oldStart: 10,
|
|
oldLines: 4,
|
|
newStart: 12,
|
|
newLines: 6,
|
|
lines: [
|
|
" <span>{count}</span>",
|
|
"- <button onClick={() => setCount(c => c + 1)}>+</button>",
|
|
"- <button onClick={() => setCount(c => c - 1)}>-</button>",
|
|
"+ <button onClick={() => setCount(c => c + step)}>",
|
|
"+ Increment by {step}",
|
|
"+ </button>",
|
|
"+ <button onClick={() => setCount(c => c - step)}>",
|
|
"+ Decrement by {step}",
|
|
"+ </button>",
|
|
" </div>",
|
|
],
|
|
},
|
|
]
|
|
|
|
const longHunk: Hunk[] = [
|
|
{
|
|
oldStart: 1,
|
|
oldLines: 30,
|
|
newStart: 1,
|
|
newLines: 32,
|
|
lines: [
|
|
" // Line 1: imports",
|
|
" import fs from 'fs'",
|
|
" import path from 'path'",
|
|
" ",
|
|
"-const VERSION = '1.0.0'",
|
|
"+const VERSION = '2.0.0'",
|
|
" ",
|
|
" function readFile(name: string) {",
|
|
" const full = path.join(__dirname, name)",
|
|
" return fs.readFileSync(full, 'utf-8')",
|
|
" }",
|
|
" ",
|
|
" function writeFile(name: string, data: string) {",
|
|
"- fs.writeFileSync(name, data)",
|
|
"+ const full = path.join(__dirname, name)",
|
|
"+ fs.writeFileSync(full, data, 'utf-8')",
|
|
" }",
|
|
" ",
|
|
" function processAll() {",
|
|
" const files = fs.readdirSync('.')",
|
|
" for (const file of files) {",
|
|
" const content = readFile(file)",
|
|
"- console.log(file, content.length)",
|
|
"+ const processed = content.trim()",
|
|
"+ console.log(file, processed.length)",
|
|
" }",
|
|
" }",
|
|
" ",
|
|
" export { readFile, writeFile, processAll }",
|
|
" export default { VERSION }",
|
|
],
|
|
},
|
|
]
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
|
|
describeNative("diff viewer", () => {
|
|
let testRoot: TestRoot
|
|
|
|
beforeEach(() => {
|
|
testRoot = createTestRoot()
|
|
})
|
|
|
|
describe("unified view", () => {
|
|
it("renders a simple unified diff with syntax highlighting", () => {
|
|
function UnifiedSimple() {
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
backgroundColor: DIFF_BG,
|
|
overflow: "scroll",
|
|
}}
|
|
>
|
|
<DiffViewer
|
|
hunks={simpleHunks}
|
|
filePath="test.ts"
|
|
splitView={false}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
testRoot.render(<UnifiedSimple />)
|
|
|
|
// Verify key text content is present
|
|
const allText = testRoot.renderer.getAllText()
|
|
// Line numbers and code should be present
|
|
expect(allText.some((t) => t.includes("const"))).toBe(true)
|
|
|
|
const path = `${SCREENSHOT_DIR}/gpuix-diff-unified-simple.png`
|
|
if (fs.existsSync(path)) fs.unlinkSync(path)
|
|
testRoot.renderer.captureScreenshot(path)
|
|
expect(fs.existsSync(path)).toBe(true)
|
|
expect(fs.statSync(path).size).toBeGreaterThan(0)
|
|
})
|
|
|
|
it("renders multi-hunk diff with hunk separators", () => {
|
|
function UnifiedMultiHunk() {
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
backgroundColor: DIFF_BG,
|
|
overflow: "scroll",
|
|
}}
|
|
>
|
|
<DiffViewer
|
|
hunks={multiHunkPatch}
|
|
filePath="counter.tsx"
|
|
splitView={false}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
testRoot.render(<UnifiedMultiHunk />)
|
|
|
|
const allText = testRoot.renderer.getAllText()
|
|
// Should contain both hunks' content
|
|
expect(allText.some((t) => t.includes("import"))).toBe(true)
|
|
expect(allText.some((t) => t.includes("button"))).toBe(true)
|
|
// Should have hunk separator
|
|
expect(allText.some((t) => t.includes("..."))).toBe(true)
|
|
|
|
const path = `${SCREENSHOT_DIR}/gpuix-diff-unified-multi.png`
|
|
if (fs.existsSync(path)) fs.unlinkSync(path)
|
|
testRoot.renderer.captureScreenshot(path)
|
|
expect(fs.existsSync(path)).toBe(true)
|
|
expect(fs.statSync(path).size).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe("split view", () => {
|
|
it("renders split diff with left/right panes", () => {
|
|
function SplitSimple() {
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
backgroundColor: DIFF_BG,
|
|
overflow: "scroll",
|
|
}}
|
|
>
|
|
<DiffViewer
|
|
hunks={simpleHunks}
|
|
filePath="test.ts"
|
|
splitView={true}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
testRoot.render(<SplitSimple />)
|
|
|
|
const allText = testRoot.renderer.getAllText()
|
|
expect(allText.some((t) => t.includes("const"))).toBe(true)
|
|
|
|
const path = `${SCREENSHOT_DIR}/gpuix-diff-split-simple.png`
|
|
if (fs.existsSync(path)) fs.unlinkSync(path)
|
|
testRoot.renderer.captureScreenshot(path)
|
|
expect(fs.existsSync(path)).toBe(true)
|
|
expect(fs.statSync(path).size).toBeGreaterThan(0)
|
|
})
|
|
|
|
it("renders multi-hunk split diff", () => {
|
|
function SplitMultiHunk() {
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
backgroundColor: DIFF_BG,
|
|
overflow: "scroll",
|
|
}}
|
|
>
|
|
<DiffViewer
|
|
hunks={multiHunkPatch}
|
|
filePath="counter.tsx"
|
|
splitView={true}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
testRoot.render(<SplitMultiHunk />)
|
|
|
|
const path = `${SCREENSHOT_DIR}/gpuix-diff-split-multi.png`
|
|
if (fs.existsSync(path)) fs.unlinkSync(path)
|
|
testRoot.renderer.captureScreenshot(path)
|
|
expect(fs.existsSync(path)).toBe(true)
|
|
expect(fs.statSync(path).size).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe("scrolling", () => {
|
|
it("scrolls through a long diff and produces different screenshots", () => {
|
|
function LongDiff() {
|
|
// Constrained height so content overflows and scrolling is needed.
|
|
// Full width fills the canvas, but short height forces scroll.
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: 300,
|
|
backgroundColor: DIFF_BG,
|
|
overflow: "scroll",
|
|
}}
|
|
>
|
|
<DiffViewer
|
|
hunks={longHunk}
|
|
filePath="utils.ts"
|
|
splitView={false}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
testRoot.render(<LongDiff />)
|
|
|
|
const pathBefore = `${SCREENSHOT_DIR}/gpuix-diff-scroll-before.png`
|
|
const pathAfter = `${SCREENSHOT_DIR}/gpuix-diff-scroll-after.png`
|
|
if (fs.existsSync(pathBefore)) fs.unlinkSync(pathBefore)
|
|
if (fs.existsSync(pathAfter)) fs.unlinkSync(pathAfter)
|
|
|
|
// Screenshot before scrolling
|
|
testRoot.renderer.captureScreenshot(pathBefore)
|
|
|
|
// Scroll down 200px inside the container
|
|
testRoot.renderer.nativeSimulateScrollWheel(450, 150, 0, -200)
|
|
|
|
// Screenshot after scrolling
|
|
testRoot.renderer.captureScreenshot(pathAfter)
|
|
|
|
// Both should exist with content
|
|
expect(fs.existsSync(pathBefore)).toBe(true)
|
|
expect(fs.existsSync(pathAfter)).toBe(true)
|
|
expect(fs.statSync(pathBefore).size).toBeGreaterThan(0)
|
|
expect(fs.statSync(pathAfter).size).toBeGreaterThan(0)
|
|
|
|
// Before and after scroll should produce different pixels
|
|
expect(
|
|
fs.readFileSync(pathBefore).equals(fs.readFileSync(pathAfter)),
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("empty state", () => {
|
|
it("renders 'No changes' for empty hunks", () => {
|
|
function EmptyDiff() {
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
backgroundColor: DIFF_BG,
|
|
}}
|
|
>
|
|
<DiffViewer hunks={[]} filePath="empty.ts" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
testRoot.render(<EmptyDiff />)
|
|
|
|
const allText = testRoot.renderer.getAllText()
|
|
expect(allText.some((t) => t.includes("No changes"))).toBe(true)
|
|
|
|
const path = `${SCREENSHOT_DIR}/gpuix-diff-empty.png`
|
|
if (fs.existsSync(path)) fs.unlinkSync(path)
|
|
testRoot.renderer.captureScreenshot(path)
|
|
expect(fs.existsSync(path)).toBe(true)
|
|
})
|
|
})
|
|
})
|