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
2 changes: 1 addition & 1 deletion src/hub/links/hub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('HubOpener', () => {
const result = await open({ timeoutMs: 300 }).done;
expect(result.kind).toBe('failed');
expect(result.kind === 'failed' ? result.message : '').toMatch(/did not answer within/);
expect(Date.now() - started).toBeGreaterThanOrEqual(300);
expect(Date.now() - started).toBeGreaterThanOrEqual(290);
await waitFor(() => pids().every((pid) => !alive(pid)), 'the SSH process to be killed');
});

Expand Down
6 changes: 6 additions & 0 deletions src/web/main/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,12 @@ input:focus-visible,
box-shadow: 0 12px 36px rgb(0 0 0 / 55%);
}

/* Framed thick in the selected tab's blue. */
.hubs-list,
.confirm-dialog.delete-worktree {
border: 3px solid var(--accent);
}

/* Undo the user agent's centring and colours of an open dialog. */
dialog.preset-picker,
dialog.close-dialog,
Expand Down
4 changes: 2 additions & 2 deletions src/web/ui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ const Hubs = ({ client }: { client: HubClient }) => {
Hubs
</button>
{open && (
<div class="hubs-list" role="menu" tabIndex={-1} onKeyDown={closeOnEscape}>
<div class="hubs-list" data-testid="hubs-list" role="menu" tabIndex={-1} onKeyDown={closeOnEscape}>
{entries.map((entry) => (
<button
key={entry.label}
Expand Down Expand Up @@ -296,7 +296,7 @@ const ConfirmDialog = ({ view, confirmation }: { view: View; confirmation: Confi
return (
<dialog
open
class="confirm-dialog"
class={confirmation.t === 'removeWorktree' ? 'confirm-dialog delete-worktree' : 'confirm-dialog'}
data-testid="confirm-dialog"
aria-label="Confirm"
tabIndex={-1}
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const TID = {
hostLogin: 'host-login',
/** Button inside a host banner: "Restart daemon" (outdated local), "Reinstall & restart" (outdated remote) or "Install" (down remote); opens the confirmation dialog, then sends `restartDaemon` or `reinstallDaemon`. */
hostAction: 'host-action',
/** In-page confirmation of a host action or a repo removal; nothing is sent before it is confirmed; text says every running terminal on the host will be killed when it lists none. */
/** In-page confirmation of a host action or a repo removal; nothing is sent before it is confirmed; text says every running terminal on the host will be killed when it lists none; for a worktree deletion only, framed 3px in the selected repo tab's background colour. */
confirmDialog: 'confirm-dialog',
/** One running terminal the page last saw on the daemon instance the host reports; text holds its worktree label and preset. */
confirmTarget: 'confirm-target',
Expand Down Expand Up @@ -132,6 +132,8 @@ export const TID = {
terminal: 'terminal',
/** Header button "Hubs", present only while there is a remote host; opens the list of `hubsEntry`. */
hubsMenu: 'hubs-menu',
/** The open Hubs menu holding the `hubsEntry` items; framed 3px in the selected repo tab's background colour. */
hubsList: 'hubs-list',
/** One per hub in the open Hubs menu, in order: text = "This machine" or the host name; `aria-current="true"` on the page's own hub. */
hubsEntry: 'hubs-entry',
/** Status line beside the Hubs menu for the latest hub opening: `Opening <host>…`, `<host> needs Tailscale login`, `<host>: <message>`, `Installing on <host>…` or `no host named <name>`. */
Expand Down
17 changes: 16 additions & 1 deletion test/e2e/fixture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test as base, expect, type BrowserContext, type ElementHandle, type Page } from '@playwright/test';
import { test as base, expect, type BrowserContext, type ElementHandle, type Locator, type Page } from '@playwright/test';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import type { DaemonClient } from '../support/daemon-client.js';
Expand Down Expand Up @@ -97,6 +97,21 @@ export const openHostAction = async (page: Page, host: number, label: string): P
await expect(page.locator(byTestId(TID.confirmDialog))).toBeVisible();
};

/** `locator`'s computed border as `"<width> <colour>"` for the top, right, bottom and left sides. */
export const borderOf = (locator: Locator): Promise<string[]> =>
locator.evaluate((el) => {
const style = getComputedStyle(el);
return ['top', 'right', 'bottom', 'left'].map((side) => {
const width = style.getPropertyValue(`border-${side}-width`);
const colour = style.getPropertyValue(`border-${side}-color`);
return `${width} ${colour}`;
});
});

/** The computed background colour of the selected repo tab. */
export const selectedTabColour = (page: Page): Promise<string> =>
page.locator(`${byTestId(TID.repoTab)}[aria-selected="true"]`).evaluate((el) => getComputedStyle(el).backgroundColor);

/** Confirms the open confirmation dialog. */
export const confirmAction = async (page: Page): Promise<void> => {
const dialog = page.locator(byTestId(TID.confirmDialog));
Expand Down
24 changes: 23 additions & 1 deletion test/e2e/hubs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ import { makeRepo } from '../support/daemon-host.js';
import { freePort, type HubHost } from '../support/hub-host.js';
import { remoteHub } from '../support/remote-hub.js';
import { byTestId, hubsEntry, repoTab, TID } from './contract.js';
import { expect, hubRequests, makeRepoWith, nodeRemote, openUi, runUi, test, writeHubConfig, type HostConfig } from './fixture.js';
import {
borderOf,
expect,
hubRequests,
makeRepoWith,
nodeRemote,
openUi,
runUi,
selectedTabColour,
test,
writeHubConfig,
type HostConfig,
} from './fixture.js';

const RESOLVE_ERROR = 'ssh: Could not resolve hostname box: Name or service not known';

Expand Down Expand Up @@ -70,6 +82,14 @@ test.describe('Hubs menu', () => {
await expect(page.locator(byTestId(TID.hubsMenu))).toBeFocused();
});

test("is framed 3px in the selected tab's colour", async ({ hub, page, ssh }) => {
ssh.failHost('box', RESOLVE_ERROR);
await home(hub, page, [{ name: 'box', ssh: 'box' }]);
await page.locator(byTestId(TID.hubsMenu)).click();
const blue = await selectedTabColour(page);
expect(await borderOf(page.locator(byTestId(TID.hubsList)))).toEqual(Array(4).fill(`3px ${blue}`));
});

test('is absent without remote hosts', async ({ hub, page }) => {
await home(hub, page, []);
await expect(page.locator(byTestId(TID.hubsMenu))).toHaveCount(0);
Expand Down Expand Up @@ -103,6 +123,8 @@ test.describe('Hubs menu', () => {
]);
await pick(page, 'box');
await landedOn(page, originOf(port));
// The fragment goes before the code is exchanged; a reload before the token arrives has no credential.
await expect(page.locator(repoTab(box.repo))).toBeVisible();

await page.reload();
await expect(page).toHaveTitle('worktree-term — box');
Expand Down
24 changes: 23 additions & 1 deletion test/e2e/sidebar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { join } from 'node:path';
import type { HubHost } from '../support/hub-host.js';
import { fakeWorktrees, git } from '../support/daemon-host.js';
import { COMMANDS, runIn } from './attention-helpers.js';
import { byTestId, termTab, TID, worktreeEntry } from './contract.js';
import { byTestId, repoTab, termTab, TID, worktreeEntry } from './contract.js';
import {
borderOf,
confirmClose,
detachedWorktree,
expect,
Expand All @@ -16,6 +17,7 @@ import {
openPage,
openUi,
ready,
selectedTabColour,
selectRepo,
selectWorktree,
test,
Expand Down Expand Up @@ -469,6 +471,26 @@ test.describe('worktree status and deletion', () => {
expect(git(repo, 'rev-parse', 'feat')).toMatch(/^[0-9a-f]{40}$/);
});

test("the deletion dialog is framed 3px in the selected tab's colour; a repo removal's is not", async ({ hub, page }) => {
const { repo, worktrees } = makeRepoWith(hub, 'app', ['feat']);
const feat = worktrees[0] ?? '';
withOrigin(hub, repo);
writeFileSync(join(feat, 'untracked'), 'new');
hub.writeRepos([repo]);
await openUi(hub, page);
const dialog = page.locator(byTestId(TID.confirmDialog));
await page.locator(worktreeEntry(feat)).click({ button: 'right' });
await page.locator(byTestId(TID.deleteWorktree)).click();
await expect(dialog).toBeVisible();
const blue = await selectedTabColour(page);
expect(await borderOf(dialog)).toEqual(Array(4).fill(`3px ${blue}`));
await dialog.locator(byTestId(TID.confirmCancel)).click();

await page.locator(repoTab(repo)).locator(byTestId(TID.removeRepo)).click();
await expect(dialog).toBeVisible();
expect((await borderOf(dialog)).every((side) => side.startsWith('1px ') && !side.endsWith(blue))).toBe(true);
});

test('deleting a detached worktree with commits says they become unreachable and names its head', async ({ hub, page }) => {
const { repo } = makeRepoWith(hub, 'app', []);
withOrigin(hub, repo);
Expand Down
Loading