Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bb743e1
Add comprehensive testing infrastructure with Vitest
jcrussell Jan 3, 2026
5cbab47
Add comprehensive Node class tests
jcrussell Jan 3, 2026
9797b39
Add Tree class basic operations tests
jcrussell Jan 3, 2026
5fde81b
Add comprehensive Tree layout algorithm tests
jcrussell Jan 3, 2026
e84ab0f
Add comprehensive Tree manipulation operations tests
jcrussell Jan 3, 2026
629c4c4
Add comprehensive WindowManager floating mode tests
jcrussell Jan 3, 2026
0171bd2
Add comprehensive WindowManager command system tests
jcrussell Jan 3, 2026
51125e9
Fix unit testing infrastructure and resolve circular dependencies
jcrussell Jan 4, 2026
57780df
Add comprehensive WindowManager gap and movement tests
jcrussell Jan 5, 2026
14f69fe
Add comprehensive WindowManager lifecycle tests
jcrussell Jan 5, 2026
ba8c4c4
Add comprehensive WindowManager workspace management tests
jcrussell Jan 5, 2026
3e8ade6
Add WindowManager pointer & focus management tests (partial)
jcrussell Jan 9, 2026
cca4770
Add comprehensive WindowManager batch float operations tests
jcrussell Jan 9, 2026
e42c730
Add WindowManager override management and resize operation tests
jcrussell Jan 10, 2026
1e8bb15
Fix WindowManager focus tests - 36/37 now passing (97%)
jcrussell Jan 10, 2026
23e0a56
Fix all 64 failing tests - test suite now 100% passing
jcrussell Jan 11, 2026
760def0
Add comprehensive tests for theme.js (56 tests)
jcrussell Jan 11, 2026
c3c8b9e
Add comprehensive tests for settings.js (31 tests)
jcrussell Jan 11, 2026
373baf6
Update COVERAGE-GAPS.md with current test status
jcrussell Jan 11, 2026
2a41cf2
Add CLAUDE.md with project guidance for Claude Code
jcrussell Jan 11, 2026
93e796d
Remove tests/COVERAGE-GAPS.md and update CLAUDE.md references
jcrussell Jan 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Testing
coverage/

# Build outputs
temp/
dist/
*.zip

# Git
.git/
.gitignore

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# GNOME extension install path
.local/

# Config
.config/

# Compiled schemas
schemas/gschemas.compiled

# Locale/translation build artifacts
po/*.mo

# Generated metadata
lib/prefs/metadata.js
16 changes: 15 additions & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,22 @@ jobs:
- name: Build Node.js project
run: npm run build --if-present --verbose

- name: Test Node.js project
- name: Run linter (prettier)
run: npm run lint --verbose

- name: Run unit tests
run: npm test --verbose

- name: Generate coverage report
run: npm run test:coverage --verbose

- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report-node-${{ matrix.node_version }}
path: coverage/
retention-days: 30

- name: Test Packaging
run: make dist
115 changes: 115 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Forge is a GNOME Shell extension providing i3/sway-style tiling window management. It supports GNOME 40+ on both X11 and Wayland, featuring tree-based tiling with horizontal/vertical split containers, stacked/tabbed layouts, vim-like keybindings, drag-and-drop tiling, and multi-monitor support.

## Build & Development Commands

```bash
# Install dependencies (Node.js 16+ and gettext required)
npm install

# Development build: compile, set debug mode, install to ~/.local/share/gnome-shell/extensions/
make dev

# Production build: compile, install, enable extension, restart shell
make prod

# Testing in nested Wayland session (no shell restart needed)
make test

# Testing on X11 (restarts gnome-shell)
make test-x

# Unit tests (mocked GNOME APIs via Vitest)
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # With coverage report

# Code formatting
npm run format # Format code with Prettier
npm run lint # Check formatting
```

## Architecture

### Entry Points

- `extension.js` - Main extension entry point, creates ForgeExtension class that manages lifecycle
- `prefs.js` - Preferences window entry point (GTK4/Adwaita)

### Core Components (lib/extension/)

- **tree.js** - Tree data structure for window layout (central to tiling logic)
- `Node` class: Represents monitors, workspaces, containers, and windows in a tree hierarchy
- `Tree` class: Manages the entire tree structure, handles layout calculations
- `Queue` class: Event queue for window operations
- Node types: ROOT, MONITOR, WORKSPACE, CON (container), WINDOW
- Layout types: HSPLIT, VSPLIT, STACKED, TABBED, PRESET

- **window.js** - WindowManager class, handles window signals, grab operations, tiling logic, and focus management

- **keybindings.js** - Keyboard shortcut management

- **utils.js** - Utility functions for geometry calculations, window operations

- **enum.js** - `createEnum()` helper for creating frozen enum objects

### Shared Modules (lib/shared/)

- **settings.js** - ConfigManager for loading window overrides from `~/.config/forge/config/windows.json`
- **logger.js** - Debug logging (controlled by settings)
- **theme.js** - ThemeManagerBase for CSS parsing and stylesheet management

### Preferences UI (lib/prefs/)

GTK4/Adwaita preference pages - not covered by unit tests.

### GSettings Schemas

Located in `schemas/org.gnome.shell.extensions.forge.gschema.xml`. Compiled during build.

## Testing Infrastructure

Tests use Vitest with mocked GNOME APIs (tests/mocks/gnome/). The mocks simulate Meta, Gio, GLib, Shell, St, Clutter, and GObject APIs so tests can run in Node.js without GNOME Shell.

**Always run tests in Docker** to ensure consistent environment:

```bash
# Run all tests in Docker (preferred)
make unit-test-docker

# Run with coverage report
make unit-test-docker-coverage

# Watch mode for development
make unit-test-docker-watch

# Run locally (if Node.js environment matches)
npm test
npm run test:coverage
```

**Coverage**: 60.5% overall, 728 tests. Run `npm run test:coverage` for detailed breakdown.

Test structure:
- `tests/setup.js` - Global test setup, loads mocks
- `tests/mocks/gnome/` - GNOME API mocks (Meta.js, GLib.js, etc.)
- `tests/mocks/helpers/` - Test helpers like `createMockWindow()`
- `tests/unit/` - Unit tests organized by module

## Key Concepts

- **Tiling tree**: Windows are organized in a tree structure similar to i3/sway. Containers can split horizontally or vertically, or display children in stacked/tabbed mode.

- **Window modes**: TILE (managed by tree), FLOAT (unmanaged), GRAB_TILE (being dragged), DEFAULT

- **Session modes**: Extension disables keybindings on lock screen but keeps tree in memory to preserve layout

## Configuration Files

- Window overrides: `~/.config/forge/config/windows.json`
- Stylesheet overrides: `~/.config/forge/stylesheet/forge/stylesheet.css`
15 changes: 15 additions & 0 deletions Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM node:20-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies (use install to update lock file with new vitest deps)
RUN npm install

# Copy source code
COPY . .

# Default command runs tests
CMD ["npm", "test"]
23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,26 @@ lint:

check:
npx prettier --check "./**/*.{js,jsx,ts,tsx,json}"

# Unit tests (local with mocked GNOME APIs)
unit-test:
npm test

unit-test-watch:
npm run test:watch

unit-test-coverage:
npm run test:coverage

# Docker-based testing (for CI or consistent environments)
docker-test-build:
docker build -f Dockerfile.test -t forge-test .

unit-test-docker: docker-test-build
docker run --rm forge-test npm test

unit-test-docker-watch: docker-test-build
docker run --rm -it -v $(PWD):/app forge-test npm run test:watch

unit-test-docker-coverage: docker-test-build
docker run --rm -v $(PWD)/coverage:/app/coverage forge-test npm run test:coverage
10 changes: 10 additions & 0 deletions lib/extension/enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Turns an array into an immutable enum-like object
*/
export function createEnum(anArray) {
const enumObj = {};
for (const val of anArray) {
enumObj[val] = val;
}
return Object.freeze(enumObj);
}
13 changes: 8 additions & 5 deletions lib/extension/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,19 @@ import St from "gi://St";
import { Logger } from "../shared/logger.js";

// App imports
import { createEnum } from "./enum.js";
import * as Utils from "./utils.js";
import * as Window from "./window.js";

export const NODE_TYPES = Utils.createEnum([
export const NODE_TYPES = createEnum([
"ROOT",
"MONITOR", //Output in i3
"CON", //Container in i3
"WINDOW",
"WORKSPACE",
]);

export const LAYOUT_TYPES = Utils.createEnum([
export const LAYOUT_TYPES = createEnum([
"STACKED",
"TABBED",
"ROOT",
Expand All @@ -47,9 +48,9 @@ export const LAYOUT_TYPES = Utils.createEnum([
"PRESET",
]);

export const ORIENTATION_TYPES = Utils.createEnum(["NONE", "HORIZONTAL", "VERTICAL"]);
export const ORIENTATION_TYPES = createEnum(["NONE", "HORIZONTAL", "VERTICAL"]);

export const POSITION = Utils.createEnum(["BEFORE", "AFTER", "UNKNOWN"]);
export const POSITION = createEnum(["BEFORE", "AFTER", "UNKNOWN"]);

/**
* The Node data representation of the following elements in the user's display:
Expand Down Expand Up @@ -118,6 +119,7 @@ export class Node extends GObject.Object {

set rect(rect) {
this._rect = rect;
if (!rect) return;
switch (this.nodeType) {
case NODE_TYPES.WINDOW:
break;
Expand Down Expand Up @@ -364,7 +366,8 @@ export class Node extends GObject.Object {
// detach only from the immediate parent
let parentNode = node.parentNode;
refNode = parentNode.childNodes.splice(node.index, 1);
refNode.parentNode = null;
// Clear the parent reference on the removed node
node.parentNode = null;
}
if (!refNode) {
throw `NodeNotFound ${node}`;
Expand Down
15 changes: 3 additions & 12 deletions lib/extension/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,14 @@ import St from "gi://St";
import { PACKAGE_VERSION } from "resource:///org/gnome/shell/misc/config.js";

// App imports
import { createEnum } from "./enum.js";
import { ORIENTATION_TYPES, LAYOUT_TYPES, POSITION } from "./tree.js";
import { GRAB_TYPES } from "./window.js";

const [major] = PACKAGE_VERSION.split(".").map((s) => Number(s));

/**
*
* Turns an array into an immutable enum-like object
*
*/
export function createEnum(anArray) {
const enumObj = {};
for (const val of anArray) {
enumObj[val] = val;
}
return Object.freeze(enumObj);
}
// Re-export createEnum for backward compatibility
export { createEnum };

export function resolveX(rectRequest, metaWindow) {
let metaRect = metaWindow.get_frame_rect();
Expand Down
5 changes: 3 additions & 2 deletions lib/extension/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { PACKAGE_VERSION } from "resource:///org/gnome/shell/misc/config.js";
import { Logger } from "../shared/logger.js";

// App imports
import { createEnum } from "./enum.js";
import * as Utils from "./utils.js";
import { Keybindings } from "./keybindings.js";
import {
Expand All @@ -47,10 +48,10 @@ import { production } from "../shared/settings.js";

/** @typedef {import('../../extension.js').default} ForgeExtension */

export const WINDOW_MODES = Utils.createEnum(["FLOAT", "TILE", "GRAB_TILE", "DEFAULT"]);
export const WINDOW_MODES = createEnum(["FLOAT", "TILE", "GRAB_TILE", "DEFAULT"]);

// Simplify the grab modes
export const GRAB_TYPES = Utils.createEnum(["RESIZING", "MOVING", "UNKNOWN"]);
export const GRAB_TYPES = createEnum(["RESIZING", "MOVING", "UNKNOWN"]);

export class WindowManager extends GObject.Object {
static {
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"description": "Forge - Tiling Extension for GNOME",
"main": "extension.js",
"scripts": {
"test": "prettier --list-different \"./**/*.{js,jsx,ts,tsx,json}\"",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "prettier --list-different \"./**/*.{js,jsx,ts,tsx,json}\"",
"prepare": "husky install",
"format": "prettier --write \"./**/*.{js,jsx,ts,tsx,json}\""
},
Expand Down Expand Up @@ -40,7 +43,9 @@
"@girs/st-12": "^12.0.0-3.2.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1"
"prettier": "^2.7.1",
"vitest": "^2.1.8",
"@vitest/coverage-v8": "^2.1.8"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
Expand Down
Loading