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
44 changes: 22 additions & 22 deletions apps/editor/e2e/git.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,24 @@ test('git: VCS menu, commit panel, log, branches, remote, conflict, amend, stash
await showPanel(page, 'Commit');
await expect(commitPanel(page).getByText('This project is not a git repository')).toBeVisible();
await vcs(page, 'Initialize repository');
await expect(group(page, 'Unversioned Files')).toBeVisible();
await expect(group(page, 'Unversioned files')).toBeVisible();
git(PONG, 'config', 'user.name', 'E2E');
git(PONG, 'config', 'user.email', 'e2e@example.com');

const nested = row(page, 'Unversioned Files', 'apps/client/src/main.ts');
const nested = row(page, 'Unversioned files', 'apps/client/src/main.ts');
await expect(nested).toContainText('main.ts');
await expect(nested).toContainText('apps/client/src');
await message(page).fill('first commit');
await expect(commitButton).toBeDisabled();
const includeAll = group(page, 'Unversioned Files').getByRole('checkbox', {
name: 'Include all: Unversioned Files',
const includeAll = group(page, 'Unversioned files').getByRole('checkbox', {
name: 'Include all: Unversioned files',
includeHidden: true,
});
await nested.getByRole('checkbox').check();
expect(await includeAll.evaluate((box: HTMLInputElement) => box.indeterminate)).toBe(true);
await includeAll.check();
await commit(page, 'first commit');
await expect(row(page, 'Unversioned Files', 'package.json')).toHaveCount(0);
await expect(row(page, 'Unversioned files', 'package.json')).toHaveCount(0);
await expect(branchStatus(page, 'main')).toBeVisible();

await showPanel(page, 'Git');
Expand Down Expand Up @@ -181,23 +181,23 @@ test('git: VCS menu, commit panel, log, branches, remote, conflict, amend, stash
await page.keyboard.press('Space');
await expect(changed.getByRole('checkbox')).toBeChecked();
await changed.click({ button: 'right' });
await expect(page.getByRole('menuitem', { name: /^Show Diff/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /^Jump to Source/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /^Show diff/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /^Jump to source/ })).toBeVisible();
await page.getByRole('menuitem', { name: /^Rollback…/ }).click();
await expect(page.getByRole('heading', { name: `Rollback ${FILE}?` })).toBeVisible();
await page.getByRole('button', { name: 'Rollback', exact: true }).last().click();
await expect(changed).toHaveCount(0);
writeFileSync(join(PONG, NEW_FILE), 'new\n');
await row(page, 'Unversioned Files', NEW_FILE).click({ button: 'right' });
await row(page, 'Unversioned files', NEW_FILE).click({ button: 'right' });
await page.getByRole('menuitem', { name: 'Add to VCS' }).click();
await expect(row(page, 'Changes', NEW_FILE)).toBeVisible();
await row(page, 'Changes', NEW_FILE).click();
await rollBack.click();
await page.getByRole('button', { name: 'Rollback', exact: true }).last().click();
await row(page, 'Unversioned Files', NEW_FILE).click();
await row(page, 'Unversioned files', NEW_FILE).click();
await page.keyboard.press('Delete');
await page.getByRole('button', { name: 'Delete', exact: true }).last().click();
await expect(row(page, 'Unversioned Files', NEW_FILE)).toHaveCount(0);
await expect(row(page, 'Unversioned files', NEW_FILE)).toHaveCount(0);

await vcs(page, 'New branch…');
await page.getByRole('textbox', { name: 'Branch name' }).fill('feature');
Expand All @@ -207,7 +207,7 @@ test('git: VCS menu, commit panel, log, branches, remote, conflict, amend, stash
await local.getByRole('button', { name: 'main', exact: true }).dblclick();
await expect(branchStatus(page, 'main')).toBeVisible();
await local.getByRole('button', { name: 'feature', exact: true }).click({ button: 'right' });
await expect(page.getByRole('menuitem', { name: "New Branch from 'feature'…" })).toBeVisible();
await expect(page.getByRole('menuitem', { name: "New branch from 'feature'…" })).toBeVisible();
await page.getByRole('menuitem', { name: 'Delete', exact: true }).click();
await page.getByRole('button', { name: 'Delete', exact: true }).last().click();
await expect(local.getByRole('button', { name: 'feature', exact: true })).toHaveCount(0);
Expand All @@ -229,7 +229,7 @@ test('git: VCS menu, commit panel, log, branches, remote, conflict, amend, stash
writeFileSync(join(OTHER, FILE), 'theirs\n');
git(OTHER, 'commit', '--quiet', '-am', 'their change');
git(OTHER, 'push', '--quiet');
await logPanel(page).getByRole('button', { name: 'Fetch All Remotes' }).click();
await logPanel(page).getByRole('button', { name: 'Fetch all remotes' }).click();
await expect(branchStatus(page, 'main ↓1')).toBeVisible();
const remote = logPanel(page).getByRole('list', { name: 'Branches of origin' });
await remote.getByRole('button', { name: 'main', exact: true }).click();
Expand All @@ -248,12 +248,12 @@ test('git: VCS menu, commit panel, log, branches, remote, conflict, amend, stash
await expect(branchStatus(page, 'main ↑1 ↓1')).toBeVisible();
await vcs(page, 'Update project');
await expect(page.getByText('The update stopped on conflicts').last()).toBeVisible();
const conflict = row(page, 'Merge Conflicts', FILE);
const conflict = row(page, 'Merge conflicts', FILE);
await expect(conflict).toBeVisible();
writeFileSync(join(PONG, FILE), 'both\n');
await conflict.getByRole('button', { name: 'Mark resolved' }).click();
await expect(group(page, 'Merge Conflicts')).toHaveCount(0);
await commit(page, 'merge', 'Commit and Push…');
await expect(group(page, 'Merge conflicts')).toHaveCount(0);
await commit(page, 'merge', 'Commit and push…');
const afterMerge = await push();
await expect(afterMerge.getByRole('list', { name: 'Commits to push' })).toContainText('merge');
await afterMerge.getByRole('button', { name: 'Push', exact: true }).click();
Expand All @@ -267,7 +267,7 @@ test('git: VCS menu, commit panel, log, branches, remote, conflict, amend, stash
await commit(page, 'merge, amended');
await expect(commits(page).getByRole('row').first()).toContainText('merge, amended');
await expect(changed).toHaveCount(0);
await commitPanel(page).getByRole('button', { name: 'Commit Message History' }).click();
await commitPanel(page).getByRole('button', { name: 'Commit message history' }).click();
await page.getByRole('menuitem', { name: 'my change' }).click();
await expect(message(page)).toHaveValue('my change');
await message(page).fill('');
Expand Down Expand Up @@ -299,9 +299,9 @@ test('browses, folds and includes changes from the keyboard', async ({ page }) =
await showPanel(page, 'Commit');
await vcs(page, 'Initialize repository');
const files = commitPanel(page).getByRole('group', { name: 'Changed files' });
const node = commitPanel(page).getByRole('button', { name: /^Unversioned Files, / });
const node = commitPanel(page).getByRole('button', { name: /^Unversioned files, / });
const includeAll = commitPanel(page).getByRole('checkbox', {
name: 'Include all: Unversioned Files',
name: 'Include all: Unversioned files',
includeHidden: true,
});
const active = () =>
Expand All @@ -314,7 +314,7 @@ test('browses, folds and includes changes from the keyboard', async ({ page }) =
await page.keyboard.press('ArrowDown');
await expect.poll(active).toMatch(/^Changes, 0 files/);
await page.keyboard.press('ArrowDown');
await expect.poll(active).toMatch(/^Unversioned Files, /);
await expect.poll(active).toMatch(/^Unversioned files, /);
await page.keyboard.press('Space');
await expect(includeAll).toBeChecked();
await page.keyboard.press('Space');
Expand All @@ -324,12 +324,12 @@ test('browses, folds and includes changes from the keyboard', async ({ page }) =
await page.keyboard.press('ArrowRight');
await expect(node).toHaveAttribute('aria-expanded', 'true');
await page.keyboard.press('ArrowRight');
await expect.poll(active).not.toMatch(/^Unversioned Files/);
await expect.poll(active).not.toMatch(/^Unversioned files/);
await expect(
group(page, 'Unversioned Files').getByRole('option', { selected: true }),
group(page, 'Unversioned files').getByRole('option', { selected: true }),
).toHaveCount(1);
await page.keyboard.press('ArrowLeft');
await expect.poll(active).toMatch(/^Unversioned Files, /);
await expect.poll(active).toMatch(/^Unversioned files, /);
await resetLayout(page);
/** The layout is saved after 500 ms. */
await page.waitForTimeout(900);
Expand Down
15 changes: 12 additions & 3 deletions apps/editor/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { type Page, expect } from '@playwright/test';

/** Puts the saved layout of the project back to the default (tests reuse projects). */
/**
* Puts the saved layout of the project back to the default with every panel (tests reuse
* projects, and expect the panels of the full layout).
*/
export const resetLayout = async (page: Page): Promise<void> => {
await page.getByRole('button', { name: 'View', exact: true }).click();
await page.getByRole('menuitem', { name: 'Layouts' }).click();
await page.getByRole('menuitem', { name: 'Reset layout' }).click();
await expect(page.getByText('Layout reset to default').last()).toBeVisible();
await page.getByRole('menuitem', { name: 'All panels' }).click();
await expect(page.getByText('Layout reset with all panels').last()).toBeVisible();
};

/** Expands folders of the Files tree, from the root (already expanded ones stay open). */
Expand All @@ -32,6 +35,12 @@ export const panelTab = (page: Page, name: string | RegExp) =>
*/
export const showPanel = async (page: Page, name: string | RegExp): Promise<void> => {
const tab = panelTab(page, name);
/** A panel the layout does not show (History, by default) opens from View › Panels. */
if (typeof name === 'string' && !(await tab.count())) {
await page.getByRole('button', { name: 'View', exact: true }).click();
await page.getByRole('menuitem', { name: 'Panels' }).click();
await page.getByRole('menuitem', { name, exact: true }).click();
}
await expect(tab).toBeVisible();
if ((await tab.getAttribute('aria-selected')) !== 'true') await tab.click();
await expect(tab).toHaveAttribute('aria-selected', 'true');
Expand Down
5 changes: 4 additions & 1 deletion apps/editor/e2e/history-panel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';

import { openProject } from './helpers';
import { openProject, resetLayout, showPanel } from './helpers';
import { type Page, expect, test } from './test';
import { PONG } from './workspace';

Expand All @@ -14,6 +14,9 @@ const row = (page: Page, name: string) => files(page).getByRole('treeitem', { na
const open = async (page: Page) => {
await openProject(page, 'pong');
await page.waitForURL(/\/project\//);
await resetLayout(page);
/** History is not shown by default. */
await showPanel(page, 'History');
await expect(row(page, 'apps')).toBeVisible();
};

Expand Down
10 changes: 6 additions & 4 deletions apps/editor/e2e/inspectors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,16 @@ test.describe('with the engine', () => {

/** The count shown; screen readers get it only once a search has settled. */
const count = page.getByText(/of \d+ packets/).first();
/**
* While hidden (still mounted), the panel records nothing: read its count twice while it is
* hidden, once the packets already on their way have landed.
*/
await showPanel(page, 'Console');
await page.waitForTimeout(1500);
await showPanel(page, 'Network');
await page.waitForTimeout(500);
const before = await count.textContent();
await showPanel(page, 'Console');
await page.waitForTimeout(2500);
await showPanel(page, 'Network');
expect(await count.textContent()).toBe(before);
await showPanel(page, 'Network');
await expect(page.getByRole('img', { name: 'Bytes per second' })).toBeVisible({
timeout: 10_000,
});
Expand Down
39 changes: 38 additions & 1 deletion apps/editor/e2e/workbench.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expandFolders, openProject, panelTab, resetLayout } from './helpers';
import { expandFolders, openProject, panelTab, resetLayout, showPanel } from './helpers';
import { type Locator, type Page, expect, test } from './test';

const openPong = async (page: Page) => {
Expand Down Expand Up @@ -55,6 +55,7 @@ test('moves, resizes and floats docks, undoes, and restores the layout after rel
}) => {
await openPong(page);

await showPanel(page, 'History');
await drag(page, panelTab(page, 'History'), panelTab(page, 'Console'));
await expect(stackOf(page, 'Console')).toHaveAttribute('data-slot', 'bottom');
await expect(page.locator('[data-slot="bottom"][data-nf-drop="stack"]').first()).toHaveAttribute(
Expand Down Expand Up @@ -196,3 +197,39 @@ test('the project name in the status bar shows the Project screen, it no longer
);
await expect(page).toHaveURL(/\/project\//);
});

test('a simple layout shows the main panels only, all panels come back from the View menu', async ({
page,
}) => {
await openPong(page);
const layouts = async (entry: string) => {
await page.getByRole('button', { name: 'View', exact: true }).click();
await page.getByRole('menuitem', { name: 'Layouts' }).click();
await page.getByRole('menuitem', { name: entry }).click();
};
const stripes = page.getByRole('navigation', { name: /tool windows$/ });
try {
await layouts('Simple layout');
await expect(page.getByText('Layout reset with the main panels').last()).toBeVisible();
for (const name of ['Files', 'Hierarchy', 'Inspector', 'Console'])
await expect(stripes.getByRole('tab', { name, exact: true })).toBeVisible();
for (const name of ['Commit', 'Profiler', 'History', 'Components'])
await expect(stripes.getByRole('tab', { name, exact: true })).toHaveCount(0);
await layouts('All panels');
await expect(stripes.getByRole('tab', { name: 'Commit', exact: true })).toBeVisible();
} finally {
await resetLayout(page);
await page.waitForTimeout(900);
}
});

test('says what each screen is for', async ({ page }) => {
await openPong(page);
const screens = page.getByRole('tablist', { name: 'Main screens' });
await expect(
screens.getByRole('tab', { name: 'Scene', exact: true }),
).toHaveAccessibleDescription('Your entities drawn in 2D: select and move them.');
await expect(
screens.getByRole('tab', { name: 'Project', exact: true }),
).toHaveAccessibleDescription(/apps and shared libraries/);
});
5 changes: 0 additions & 5 deletions apps/editor/src/lib/editor/error-description.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { RpcError } from '@nanoforge-dev/editor-rpc';

export interface ErrorAction {
readonly label: string;
readonly run: () => void | Promise<void>;
Expand All @@ -14,8 +12,5 @@ export interface ErrorDescription {
readonly actions: readonly ErrorAction[];
}

export const errorCode = (reason: unknown): string | undefined =>
reason instanceof RpcError ? reason.code : (reason as { code?: string } | null)?.code;

export const errorMessage = (reason: unknown): string =>
reason instanceof Error ? reason.message : String(reason);
1 change: 1 addition & 0 deletions apps/editor/src/lib/project/register-core-panels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const PANELS = [
{
id: 'core.project',
title: 'Project',
description: 'Your apps and shared libraries, and what each one uses.',
icon: 'layers',
kind: 'screen',
order: 100,
Expand Down
6 changes: 4 additions & 2 deletions apps/editor/src/lib/workspace/workspace-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
import { type AppModel, ProjectsContract } from '@nanoforge-dev/editor-protocol';
import type { RpcClient } from '@nanoforge-dev/editor-rpc';
import type { NotificationService, PromptService } from '@nanoforge-dev/editor-ui';
/** The notification helpers alone: the kit's main entry brings its Svelte components. */
import { notifyError } from '@nanoforge-dev/editor-ui/notification';

/** Undo and redo target of the Project screen: changes of the workspace. */
export const PROJECT_HISTORY_CONTEXT = 'project';
Expand Down Expand Up @@ -269,8 +271,8 @@ export class WorkspaceActions implements Disposable {
detail: 'Its output is in the Console, under Tasks.',
});
} catch (error) {
notifications.notify('error', 'Could not install the dependencies', {
detail: error instanceof Error ? error.message : String(error),
notifyError(notifications, 'Could not install the dependencies', error, {
retry: () => this.install(),
});
}
}
Expand Down
7 changes: 4 additions & 3 deletions apps/editor/src/routes/(editor)/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script lang="ts">
import { errorCodeOf } from '@nanoforge-dev/editor-kernel';
import type { Snippet } from 'svelte';

import Loading from '$lib/editor/Loading.svelte';
import { type Editor, createEditor } from '$lib/editor/create-editor';
import { provideEditor } from '$lib/editor/editor-context';
import { type ErrorDescription, errorCode, errorMessage } from '$lib/editor/error-description';
import { type ErrorDescription, errorMessage } from '$lib/editor/error-description';

const { children }: { children: Snippet } = $props();

Expand All @@ -19,14 +20,14 @@

/** Startup failures, with what the user can do about them. */
const describeStartupError = (reason: unknown): ErrorDescription =>
errorCode(reason) === 'TIMEOUT'
errorCodeOf(reason) === 'TIMEOUT'
? {
title: 'The editor server does not answer',
hint: 'It accepted the connection but sent nothing back. Restart it with nf editor, then try again.',
detail: errorMessage(reason),
actions: [retry],
}
: errorCode(reason) === 'UNAVAILABLE'
: errorCodeOf(reason) === 'UNAVAILABLE'
? {
title: "Can't reach the editor server",
hint: 'Start it with nf editor in your project folder (or check your connection), then try again.',
Expand Down
4 changes: 2 additions & 2 deletions apps/editor/src/routes/(editor)/load/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { errorCodeOf } from '@nanoforge-dev/editor-kernel';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import type { ProjectRef } from '@nanoforge-dev/editor-protocol';
Expand All @@ -8,7 +9,6 @@
import {
type ErrorAction,
type ErrorDescription,
errorCode,
errorMessage,
} from '$lib/editor/error-description';

Expand Down Expand Up @@ -55,7 +55,7 @@
const describe = (reason: unknown, ref: ProjectRef): ErrorDescription => {
const detail = errorMessage(reason);
const name = 'path' in ref ? ref.path : 'this project';
switch (errorCode(reason)) {
switch (errorCodeOf(reason)) {
case 'NOT_FOUND':
return 'path' in ref
? {
Expand Down
Loading
Loading