diff --git a/.github/workflows/build-core.yml b/.github/workflows/build-core.yml index 6464254a2..0e3b108c4 100644 --- a/.github/workflows/build-core.yml +++ b/.github/workflows/build-core.yml @@ -7,8 +7,8 @@ on: jobs: build: - name: Core - Build and Test - runs-on: ubuntu-latest + name: Build Core Library + runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v4 @@ -31,7 +31,62 @@ jobs: cd packages/core bun run build - - name: Run tests + - name: Run native tests run: | cd packages/core - bun run test + bun run test:native + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + packages/core/dist + packages/core/node_modules/@opentui/core-* + retention-days: 1 + + test: + name: Test - ${{ matrix.os }} (${{ matrix.arch }}) + needs: build + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - os: macos + arch: arm64 + runner: macos-latest + - os: macos + arch: x64 + runner: macos-13 + - os: linux + arch: x64 + runner: ubuntu-latest + - os: linux + arch: arm64 + runner: ubuntu-24.04-arm + - os: windows + arch: x64 + runner: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: packages/core + + - name: Install dependencies + run: bun install + + - name: Run TypeScript tests + run: | + cd packages/core + bun run test:js diff --git a/packages/core/src/lib/tree-sitter/cache.test.ts b/packages/core/src/lib/tree-sitter/cache.test.ts index fefdc0426..e602cb061 100644 --- a/packages/core/src/lib/tree-sitter/cache.test.ts +++ b/packages/core/src/lib/tree-sitter/cache.test.ts @@ -212,7 +212,9 @@ describe("TreeSitterClient Caching", () => { }) test("should handle directory creation errors gracefully", async () => { - const invalidDataPath = "/invalid/path/that/cannot/be/created" + // Use a null byte in the path to ensure it is invalid on all platforms. + // This helps test error handling for directory creation in a cross-platform way. + const invalidDataPath = "/invalid\x00/path/with/null/byte" const client = new TreeSitterClient({ dataPath: invalidDataPath }) await expect(client.initialize()).rejects.toThrow() diff --git a/packages/core/src/lib/tree-sitter/client.ts b/packages/core/src/lib/tree-sitter/client.ts index 2cb702c43..c3306d51c 100644 --- a/packages/core/src/lib/tree-sitter/client.ts +++ b/packages/core/src/lib/tree-sitter/client.ts @@ -12,10 +12,9 @@ import type { SimpleHighlight, } from "./types" import { getParsers } from "./default-parsers" -import { resolve, isAbsolute } from "path" +import { resolve, isAbsolute, parse } from "path" import { existsSync } from "fs" import { registerEnvVar, env } from "../env" -import { parse } from "path" registerEnvVar({ name: "OTUI_TREE_SITTER_WORKER_PATH", diff --git a/packages/core/src/renderables/Text.test.ts b/packages/core/src/renderables/Text.test.ts index 7a66adb8b..de216adc3 100644 --- a/packages/core/src/renderables/Text.test.ts +++ b/packages/core/src/renderables/Text.test.ts @@ -32,6 +32,8 @@ describe("TextRenderable Selection", () => { }) await currentMouse.drag(text.x, text.y, text.x + 5, text.y) + // Add delay to ensure all drag events are processed on slow CI machines + await new Promise((resolve) => setTimeout(resolve, 50)) await renderOnce() const selectedText = text.getSelectedText() @@ -46,6 +48,8 @@ describe("TextRenderable Selection", () => { // Select "Hello 🌍" (7 characters: H,e,l,l,o, ,🌍) await currentMouse.drag(text.x, text.y, text.x + 7, text.y) + // Add delay to ensure all drag events are processed on slow CI machines + await new Promise((resolve) => setTimeout(resolve, 50)) await renderOnce() const selectedText = text.getSelectedText() diff --git a/packages/core/src/testing/mock-keys.test.ts b/packages/core/src/testing/mock-keys.test.ts index 7293bec91..0cecb6373 100644 --- a/packages/core/src/testing/mock-keys.test.ts +++ b/packages/core/src/testing/mock-keys.test.ts @@ -184,7 +184,7 @@ describe("mock-keys", () => { expect(timestamps).toHaveLength(2) expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(8) // Allow some tolerance - expect(timestamps[1] - timestamps[0]).toBeLessThan(20) + expect(timestamps[1] - timestamps[0]).toBeLessThan(50) // Increased tolerance for CI/slower machines }) test("pressKey with shift modifier", () => { diff --git a/packages/core/src/zig/renderer.zig b/packages/core/src/zig/renderer.zig index f3f921105..b1daa8fed 100644 --- a/packages/core/src/zig/renderer.zig +++ b/packages/core/src/zig/renderer.zig @@ -39,6 +39,32 @@ pub const DebugOverlayCorner = enum { bottomRight, }; +const StdoutWriter = union(enum) { + real: std.io.BufferedWriter(4096, std.fs.File.Writer), + null: void, + + pub fn writer(self: *StdoutWriter) Writer { + return .{ .context = self }; + } + + pub fn flush(self: *StdoutWriter) !void { + switch (self.*) { + .real => |*w| try w.flush(), + .null => {}, + } + } + + const WriteError = std.fs.File.WriteError; + const Writer = std.io.Writer(*StdoutWriter, WriteError, write); + + fn write(self: *StdoutWriter, data: []const u8) WriteError!usize { + switch (self.*) { + .real => |*w| return w.writer().write(data), + .null => return data.len, + } + } +}; + pub const CliRenderer = struct { width: u32, height: u32, @@ -80,7 +106,7 @@ pub const CliRenderer = struct { lastRenderTime: i64, allocator: Allocator, renderThread: ?std.Thread = null, - stdoutWriter: std.io.BufferedWriter(4096, std.fs.File.Writer), + stdoutWriter: StdoutWriter, debugOverlay: struct { enabled: bool, corner: DebugOverlayCorner, @@ -141,17 +167,9 @@ pub const CliRenderer = struct { const currentBuffer = try OptimizedBuffer.init(allocator, width, height, .{ .pool = pool, .width_method = .unicode, .id = "current buffer" }, graphemes_data, display_width); const nextBuffer = try OptimizedBuffer.init(allocator, width, height, .{ .pool = pool, .width_method = .unicode, .id = "next buffer" }, graphemes_data, display_width); - const stdoutWriter = if (testing) blk: { - // In testing mode, use /dev/null to discard output - const devnull = std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }) catch { - // Fallback to stdout if /dev/null can't be opened - logger.warn("Failed to open /dev/null, falling back to stdout\n", .{}); - break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = std.io.getStdOut().writer() }; - }; - break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = devnull.writer() }; - } else blk: { + const stdoutWriter: StdoutWriter = if (testing) .{ .null = {} } else blk: { const stdout = std.io.getStdOut(); - break :blk std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = stdout.writer() }; + break :blk .{ .real = std.io.BufferedWriter(4096, std.fs.File.Writer){ .unbuffered_writer = stdout.writer() } }; }; // stat sample arrays