Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
run: npm run build

# Navigation smoke tests ([WEBSITE-E2E-SMOKE]). Both presets (Desktop
# Chrome + Pixel 5) run on Chromium, so only chromium is installed.
- name: Install Playwright browser
working-directory: website
run: npx playwright install --with-deps chromium

- name: Run navigation smoke tests (desktop + mobile)
working-directory: website
# CI uses the stdout `list` reporter only — no HTML report, trace,
# video or screenshot is produced or uploaded ([GITHUB-NO-ARTIFACTS]).
run: npm run test:e2e

# ── Lint (runs in parallel with all test jobs) ─────────────────────────────
lint:
name: Lint
Expand Down
1 change: 1 addition & 0 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Specifications define the target behavior and architecture. They are the source
| [ZED-SPEC.md](specs/ZED-SPEC.md) | Zed extension (WASM) — LSP integration, tree-sitter grammars, DAP debugging. |
| [LSP-TEST-INTEGRATION-SPEC.md](specs/LSP-TEST-INTEGRATION-SPEC.md) | Test discovery, execution, and editor integration — pytest/unittest, TestItem model, coverage overlay. |
| [EXTENSION-ACTIVITY-PANEL-SPEC.md](specs/EXTENSION-ACTIVITY-PANEL-SPEC.md) | Cross-editor activity panel — module explorer, type health, feature dashboard (VS Code, Zed, Neovim). |
| [WEBSITE-E2E-SPEC.md](specs/WEBSITE-E2E-SPEC.md) | Website navigation/e2e smoke tests (Playwright, desktop + mobile) — top-nav resolution, docs sidebar, and the mobile docs-submenu reachability guard. |

## Plans

Expand Down
12 changes: 8 additions & 4 deletions docs/specs/EXTENSION-ACTIVITY-PANEL-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ name into path segments and threading it into a node trie

**Flat view (`flat`, opt-in toggle).** Flat view drops the folder nesting and
lists **every module** as one sortable row labelled by its full dotted name,
ordered by the sort toggle (worst/best/alpha). It is "flat" only in that folders
ordered by the selected sort mode (module name / path / type coverage — #189).
It is "flat" only in that folders
are not nested — symbols still expand **under their owning module** and are
**never** dumped bare at the tree root (the #149 §2 flat-mode defect). The
default view is always the nested tree.
Expand Down Expand Up @@ -292,7 +293,7 @@ default view is always the nested tree.
| Collapse All | VS Code's **native** `showCollapseAll` button — never a contributed command. A custom collapse command alongside it is a duplicate (issue #113). |
| Filter | Toggle filter input to search modules/symbols by name |
| Toggle View | Switch between tree (nested folder/package hierarchy, default) and flat (every module as one sortable row) |
| Sort | Cycle worst-first -> best-first -> alphabetical. Applied only in flat view; tree view stays structural. Its toolbar entry is **gated on `basilisk.moduleExplorerView == 'flat'`** so it is hidden in tree view rather than rendering as a silent no-op (issue #151). Carried over from the merged Type Health panel. |
| Sort | Open an explicit picker of three labelled modes — **Module Name**, **Path**, **Type Coverage** — with the active mode checked, so the current sort is always visible (no blind cycle, issue #189). Coverage sorts ascending (least-typed first), the default. Applied only in flat view; tree view stays structural. Its toolbar entry is **gated on `basilisk.moduleExplorerView == 'flat'`** so it is hidden in tree view rather than rendering as a silent no-op (issue #151). Carried over from the merged Type Health panel. |
| Fix All | Run `basilisk.fixWorkspace`. Promoted from the info panel (issue #103); `when`-gated on `basilisk.serverState == 'running'` **and** the `config.basilisk.experimental.fixAll` flag (default off, issue #113). |
| Organize Imports | Run `basilisk.organizeImports`. Same promotion + gating. |
| Restart Server | Run `basilisk.restartServer`. Same promotion + gating. |
Expand Down Expand Up @@ -338,8 +339,11 @@ At-a-glance view of how well-typed the codebase is. Answers: "How much of my cod
> command, `TypeHealthResponse`, and the tree structure below remain the **shared
> health surface** for editors without a unified panel (Zed `/health`, Neovim
> `:BasiliskHealth`), computed from the same per-file figures as the folded rollup.
> The icon thresholds, coverage bar, `[adopted]` badge, and worst-first sort
> described here all carry over to the merged panel.
> The icon thresholds, coverage bar, and `[adopted]` badge described here all
> carry over to the merged panel — whose flat-view sort is the explicit
> Module Name / Path / Type Coverage picker
> ([EXTACT-MODULES-TOOLBAR](#EXTACT-MODULES-TOOLBAR), #189), defaulting to
> least-typed-first.

### Tree Structure {#EXTACT-HEALTH-TREE-STRUCTURE}

Expand Down
60 changes: 60 additions & 0 deletions docs/specs/WEBSITE-E2E-SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Website: Navigation & End-to-End Smoke Tests {#WEBSITE-E2E}

**Version**: 0.1.0
**Status**: Active
**License**: MIT

---

## Purpose {#WEBSITE-E2E-PURPOSE}

The marketing/docs site (`website/`) is a statically generated Eleventy build.
Before this spec, CI built the site but never exercised it, so navigation
regressions shipped silently. This spec defines browser smoke tests that run the
**production build** of `_site/` on both a desktop and a real phone viewport, so
the core "can a visitor actually get around the site" guarantees are enforced in
CI.

## Smoke Coverage {#WEBSITE-E2E-SMOKE}

Implemented by `website/tests/e2e/navigation.spec.ts`, driven by
`website/playwright.config.ts` (two projects: `desktop` = Desktop Chrome,
`mobile` = Pixel 5) and served by the dependency-free static server
`website/tests/static-server.js`. Run with `npm run test:e2e`
(`test:e2e:ui` locally).

The suite asserts, on each relevant viewport:

- **Top navigation resolves** — the home page links to Docs, Rules, Blog and
GitHub (matched by `href`, so the check holds even where the nav is collapsed
behind the hamburger on a phone).
- **Docs landing page loads** — `/docs/` renders with the docs sidebar present.
- **Desktop sidebar** — the docs sidebar is permanently visible and navigates
between sections without any toggle.
- **Mobile docs submenu** — see [WEBSITE-MOBILE-DOCS-NAV].
- **Mobile top nav** — the hamburger reveals the collapsed top nav.

### CI constraint {#WEBSITE-E2E-NO-ARTIFACTS}

Per `[GITHUB-NO-ARTIFACTS]`, the CI run emits only the stdout `list` reporter.
No Playwright HTML report, trace, video or screenshot is produced or uploaded —
those (HTML report + on-retry trace) are reserved for local runs and are
git-ignored (`website/.gitignore`). The website CI job
(`.github/workflows/ci.yml`) installs only the Chromium browser, since both
presets run on Chromium.

## Mobile Docs Submenu Reachability {#WEBSITE-MOBILE-DOCS-NAV}

On phones (`max-width: 768px`) the docs section sidebar collapses so the article
body is readable. It **must** remain reachable: the hamburger toggle
(`mobile-menu.js`, which adds `.open` to `.sidebar`) reveals it via the CSS rule
`.sidebar.open { display: block; }` in `website/src/assets/css/styles.css`,
mirroring the existing `.nav-links.open` rule for the top nav.

Without that reveal rule the JS toggle has no visual effect and the per-section
submenu (Installation, Quick Start, Configuration, Diagnostics, Reference, …) is
unreachable on a phone — the regression tracked as issue #186. The guard test is
`"docs section submenu is reachable via the hamburger"` in
`website/tests/e2e/navigation.spec.ts`: it asserts the submenu is hidden by
default, becomes visible after the hamburger is tapped, and navigates to the
chosen section.
2 changes: 1 addition & 1 deletion vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
},
{
"command": "basilisk.sortModuleExplorer",
"title": "Toggle Sort Order",
"title": "Sort Modules…",
"category": "Basilisk",
"icon": "$(sort-precedence)"
},
Expand Down
58 changes: 44 additions & 14 deletions vscode-extension/src/module-explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,20 @@ export function workspaceHealthBadge(stats: HealthStats | undefined): vscode.Vie
/** View mode for module explorer: tree (hierarchical) or flat (all symbols). */
type ViewMode = "tree" | "flat";

/** Sort mode applied in flat view (tree view stays structural). */
type SortMode = "worst" | "best" | "alpha";
/** Sort mode applied in flat view (tree view stays structural) — #189. */
type SortMode = "name" | "path" | "coverage";

const SORT_CYCLE: readonly SortMode[] = ["worst", "best", "alpha"];
/**
* The three explicit, labelled sort options surfaced in the picker (#189),
* replacing the old blind worst/best/alpha cycle. `coverage` is labelled "Type
* Coverage" to match the panel's existing "Coverage"/"% typed" wording (the
* `coveragePercent` field is type-coverage, not the PEP conformance score).
*/
const SORT_OPTIONS: readonly { readonly mode: SortMode; readonly label: string }[] = [
{ mode: "name", label: "Module Name" },
{ mode: "path", label: "Path" },
{ mode: "coverage", label: "Type Coverage" },
];

export class ModuleExplorerProvider implements vscode.TreeDataProvider<TreeItem>, vscode.Disposable {
private readonly emitter = new vscode.EventEmitter<TreeItem | undefined>();
Expand All @@ -329,7 +339,7 @@ export class ModuleExplorerProvider implements vscode.TreeDataProvider<TreeItem>
private workspace: HealthStats | undefined;
public readonly disposables: vscode.Disposable[] = [];
private viewMode: ViewMode = "tree";
private sortMode: SortMode = "worst";
private sortMode: SortMode = "coverage";
private filterPattern = "";
private treeView: vscode.TreeView<TreeItem> | undefined;

Expand All @@ -346,13 +356,22 @@ export class ModuleExplorerProvider implements vscode.TreeDataProvider<TreeItem>
this.emitter.fire(undefined);
}

/** Cycle the flat-view sort: worst-first -> best-first -> alphabetical. */
public cycleSortMode(): void {
const idx = SORT_CYCLE.indexOf(this.sortMode);
this.sortMode = SORT_CYCLE[(idx + 1) % SORT_CYCLE.length];
/** The active flat-view sort mode (surfaced in the picker, #189). */
public getSortMode(): SortMode {
return this.sortMode;
}

/** Select the flat-view sort mode explicitly and re-render (#189). */
public setSortMode(mode: SortMode): void {
this.sortMode = mode;
this.emitter.fire(undefined);
}

/** Labelled sort options with the active one marked, to drive the picker (#189). */
public sortOptions(): readonly { readonly mode: SortMode; readonly label: string; readonly current: boolean }[] {
return SORT_OPTIONS.map((option) => ({ ...option, current: option.mode === this.sortMode }));
}

/** Toggle between tree and flat view modes, persisted in workspaceState. */
public toggleViewMode(context: vscode.ExtensionContext): void {
this.viewMode = this.viewMode === "tree" ? "flat" : "tree";
Expand Down Expand Up @@ -496,12 +515,13 @@ export class ModuleExplorerProvider implements vscode.TreeDataProvider<TreeItem>
});
}

/** Order modules for flat view per the current sort toggle. */
/** Order modules for flat view per the current sort selection (#189). */
private sortModules(modules: ModuleNode[]): ModuleNode[] {
switch (this.sortMode) {
case "worst": return modules.sort((a, b) => a.coveragePercent - b.coveragePercent);
case "best": return modules.sort((a, b) => b.coveragePercent - a.coveragePercent);
case "alpha": return modules.sort((a, b) => a.name.localeCompare(b.name));
case "name": return modules.sort((a, b) => a.name.localeCompare(b.name));
case "path": return modules.sort((a, b) => a.path.localeCompare(b.path));
// Ascending coverage surfaces the least-typed modules first.
case "coverage": return modules.sort((a, b) => a.coveragePercent - b.coveragePercent);
}
}

Expand Down Expand Up @@ -580,8 +600,18 @@ function registerExplorerCommands(
vscode.commands.registerCommand("basilisk.toggleModuleExplorerView", () => {
provider.toggleViewMode(context);
}),
vscode.commands.registerCommand("basilisk.sortModuleExplorer", () => {
provider.cycleSortMode();
vscode.commands.registerCommand("basilisk.sortModuleExplorer", async () => {
// Explicit picker with the active mode checked, so the current sort is
// always visible — never a blind cycle (#189).
const items = provider.sortOptions().map((option) => ({
label: option.current ? `$(check) ${option.label}` : option.label,
mode: option.mode,
}));
const choice = await vscode.window.showQuickPick(items, {
title: "Sort Modules",
placeHolder: "Sort the flat module list by…",
});
if (choice !== undefined) { provider.setSortMode(choice.mode); }
}),
vscode.commands.registerCommand("basilisk.filterModuleExplorer", async () => {
const input = await vscode.window.showInputBox({
Expand Down
14 changes: 12 additions & 2 deletions vscode-extension/src/test/suite/activity-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,18 @@ suite("Basilisk Activity Panel E2E Tests", function () {
await vscode.commands.executeCommand("basilisk.toggleModuleExplorerView");
});

test("sortModuleExplorer command is executable", async function () {
await vscode.commands.executeCommand("basilisk.sortModuleExplorer");
test("sortModuleExplorer command opens the sort picker (#189)", async function () {
// The command now shows a QuickPick of the explicit sort modes; dismiss it
// so the test exercises the command without blocking on user input.
const dismiss = new Promise<void>((resolve) => {
setTimeout(() => {
void vscode.commands.executeCommand("workbench.action.closeQuickOpen").then(() => { resolve(); });
}, 200);
});
await Promise.all([
vscode.commands.executeCommand("basilisk.sortModuleExplorer"),
dismiss,
]);
});

// ── Info Panel Commands ───────────────────────────────────────────────
Expand Down
70 changes: 55 additions & 15 deletions vscode-extension/src/test/suite/module-explorer-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ function sym(name: string): TestSymbol {
function mod(
name: string,
kind: "package" | "module",
opts: { coverage: number; symbols?: readonly TestSymbol[]; errors?: number; warnings?: number },
opts: { coverage: number; symbols?: readonly TestSymbol[]; errors?: number; warnings?: number; path?: string },
): TestModule {
return {
name,
kind,
symbols: opts.symbols ?? [],
coveragePercent: opts.coverage,
path: `/ws/${name.split(".").join("/")}.py`,
path: opts.path ?? `/ws/${name.split(".").join("/")}.py`,
errors: opts.errors ?? 0,
warnings: opts.warnings ?? 0,
adopted: false,
Expand Down Expand Up @@ -222,33 +222,73 @@ suite("Module Explorer tree structure [EXTACT-MODULES-TREE-STRUCTURE]", () => {
}
});

test("flat-view sort toggle visibly reorders the module list — never a no-op (#151)", async () => {
test("flat-view exposes explicit name/path/coverage sort modes with a visible active mode (#151, #189)", async () => {
const provider = new ModuleExplorerProvider(storeWith(MODULES));
try {
await provider.getChildren();
provider.toggleViewMode(FAKE_CONTEXT); // -> flat

const worst = labelsOf(await provider.getChildren());
// Default surfaces the least-typed modules first (ascending coverage).
assert.strictEqual(provider.getSortMode(), "coverage", "default flat sort is by coverage");
const byCoverage = labelsOf(await provider.getChildren());
assert.deepStrictEqual(
worst,
byCoverage,
["app.models.user", "app.api.auth", "app.api", "app", "util"],
"worst-first orders by ascending coverage (30, 50, 80, 90, 100)",
"coverage sort orders by ascending coverage (30, 50, 80, 90, 100)",
);

provider.cycleSortMode(); // worst -> best
const best = labelsOf(await provider.getChildren());
provider.setSortMode("name");
const byName = labelsOf(await provider.getChildren());
assert.deepStrictEqual(
best,
["util", "app", "app.api", "app.api.auth", "app.models.user"],
"best-first orders by descending coverage",
byName,
["app", "app.api", "app.api.auth", "app.models.user", "util"],
"name sort orders alphabetically by dotted module name",
);
assert.notDeepStrictEqual(byName, byCoverage, "switching sort must change the rendered order");

provider.setSortMode("path");
assert.strictEqual(provider.getSortMode(), "path", "explicit selection sticks");

// The three modes are explicit + labelled, and the active one is marked —
// never a blind toggle (#189).
const options = provider.sortOptions();
assert.deepStrictEqual(
options.map((option) => option.label),
["Module Name", "Path", "Type Coverage"],
"exactly the three labelled sort modes are offered, in order",
);
assert.notDeepStrictEqual(best, worst, "toggling sort must change the rendered order");
assert.deepStrictEqual(
options.filter((option) => option.current).map((option) => option.mode),
["path"],
"exactly the active mode is marked current so the picker can show it",
);
} finally {
provider.dispose();
}
});

test("flat-view offers an explicit sort-by-path mode (#189)", async () => {
// Paths are chosen so file-path order (a/ < b/ < c/) differs from BOTH name
// order (alpha < beta < gamma) and score order (10 < 50 < 90) — so only a
// genuine path sort can produce [beta, alpha, gamma].
const byPath: readonly TestModule[] = [
mod("beta", "module", { coverage: 10, path: "/ws/a/beta.py" }),
mod("alpha", "module", { coverage: 90, path: "/ws/b/alpha.py" }),
mod("gamma", "module", { coverage: 50, path: "/ws/c/gamma.py" }),
];
const provider = new ModuleExplorerProvider(storeWith(byPath));
try {
await provider.getChildren();
provider.toggleViewMode(FAKE_CONTEXT); // -> flat

// #189 replaces the blind worst/best/alpha cycle with explicit
// name/path/coverage modes; selecting "path" sorts by file path.
provider.setSortMode("path");

provider.cycleSortMode(); // best -> alpha
assert.deepStrictEqual(
labelsOf(await provider.getChildren()),
["app", "app.api", "app.api.auth", "app.models.user", "util"],
"alphabetical orders by module name",
["beta", "alpha", "gamma"],
"path sort orders modules by file path, distinct from name/score order (#189)",
);
} finally {
provider.dispose();
Expand Down
5 changes: 5 additions & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ _site/
.eleventy-cache/
.DS_Store
*.local

# Playwright local outputs — never committed, never a CI artifact ([GITHUB-NO-ARTIFACTS])
test-results/
playwright-report/
playwright/.cache/
Loading
Loading