Files
bruno/tests/workspace/git-backed-collections/git-backed-collections.spec.ts
2026-05-14 17:38:55 +05:30

286 lines
13 KiB
TypeScript

import path from 'path';
import fs from 'fs';
import yaml from 'js-yaml';
import { test, expect, closeElectronApp } from '../../../playwright';
import { switchWorkspace, createCollection, waitForReadyPage } from '../../utils/page';
type CollectionEntry = { name?: string; path?: string; remote?: string };
type WorkspaceConfig = { collections?: CollectionEntry[] };
const initUserDataPath = path.join(__dirname, 'init-user-data');
const fixturesPath = path.join(__dirname, 'fixtures');
const REMOTE_URL = 'https://github.com/usebruno/sample-collection.git';
const FIXTURE_WS_NAME = 'Fixture WS';
const GHOST_WS_NAME = 'Ghost WS';
const SAMPLE_COLL_GITIGNORE_LINE = 'collections/sample-coll/';
async function copyFixture(fixtureName: string, destDir: string): Promise<string> {
const src = path.join(fixturesPath, fixtureName);
await fs.promises.cp(src, destDir, { recursive: true });
return destDir;
}
function readWorkspaceYml(workspacePath: string): WorkspaceConfig {
const raw = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8');
return yaml.load(raw) as WorkspaceConfig;
}
function readGitignoreLines(workspacePath: string): string[] {
const gitignorePath = path.join(workspacePath, '.gitignore');
if (!fs.existsSync(gitignorePath)) return [];
return fs.readFileSync(gitignorePath, 'utf8').split('\n');
}
test.describe('Git-backed collections', () => {
test.describe('Workspace overview', () => {
test('connect to Git updates workspace.yml, shows badge + remote URL, and adds .gitignore entry', async ({ launchElectronApp, createTmpDir }) => {
const workspacePath = await createTmpDir('git-ws-connect');
await copyFixture('workspace-with-collection', workspacePath);
const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } });
const page = await waitForReadyPage(app);
await switchWorkspace(page, FIXTURE_WS_NAME);
await test.step('Navigate to workspace overview', async () => {
await page.locator('.titlebar-left .home-button').click();
});
const card = page.locator('.collection-card').filter({ hasText: 'SampleColl' });
await test.step('Open Connect to Git modal from collection menu', async () => {
await card.waitFor({ state: 'visible', timeout: 5000 });
await card.locator('.collection-menu').click();
await page.locator('.dropdown-item').filter({ hasText: 'Connect to Git' }).click();
});
await test.step('Submit the modal with a Git URL', async () => {
const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Connect to Git' });
await modal.waitFor({ state: 'visible', timeout: 5000 });
await modal.locator('#remoteUrl').fill(REMOTE_URL);
await modal.getByRole('button', { name: 'Connect', exact: true }).click();
await expect(page.getByText('Git remote connected')).toBeVisible({ timeout: 10000 });
});
await test.step('Verify Git badge and remote URL render on the card', async () => {
await expect(card.locator('.collection-remote')).toContainText(REMOTE_URL, { timeout: 5000 });
await expect(card.getByText('Git', { exact: true })).toBeVisible();
});
await test.step('Verify workspace.yml records the remote on the matching entry', async () => {
const config = readWorkspaceYml(workspacePath);
const entry = config.collections?.find((c) => c.name === 'SampleColl');
expect(entry?.remote).toBe(REMOTE_URL);
});
await test.step('Verify .gitignore contains the collection path', async () => {
expect(readGitignoreLines(workspacePath)).toContain(SAMPLE_COLL_GITIGNORE_LINE);
});
await closeElectronApp(app);
});
test('remove Git remote clears the badge, the workspace.yml field, and the .gitignore line', async ({ launchElectronApp, createTmpDir }) => {
const workspacePath = await createTmpDir('git-ws-disconnect');
await copyFixture('workspace-with-collection', workspacePath);
const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } });
const page = await waitForReadyPage(app);
await switchWorkspace(page, FIXTURE_WS_NAME);
const card = page.locator('.collection-card').filter({ hasText: 'SampleColl' });
await test.step('Connect the collection to Git first', async () => {
await page.locator('.titlebar-left .home-button').click();
await card.locator('.collection-menu').click();
await page.locator('.dropdown-item').filter({ hasText: 'Connect to Git' }).click();
const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Connect to Git' });
await modal.locator('#remoteUrl').fill(REMOTE_URL);
await modal.getByRole('button', { name: 'Connect', exact: true }).click();
await expect(page.getByText('Git remote connected')).toBeVisible({ timeout: 10000 });
});
await test.step('Open Remove Git Remote modal', async () => {
await card.locator('.collection-menu').click();
await page.locator('.dropdown-item').filter({ hasText: 'Remove Git Remote' }).click();
const removeModal = page.locator('.bruno-modal-card').filter({ hasText: 'Remove Git Remote' });
await removeModal.waitFor({ state: 'visible', timeout: 5000 });
await removeModal.getByRole('button', { name: 'Remove', exact: true }).click();
await expect(page.getByText('Git remote removed')).toBeVisible({ timeout: 10000 });
});
await test.step('Verify badge and remote URL line are gone from the card', async () => {
await expect(card.locator('.collection-remote')).toHaveCount(0, { timeout: 5000 });
await expect(card.getByText('Git', { exact: true })).toHaveCount(0);
});
await test.step('Verify workspace.yml no longer carries the remote field', async () => {
const config = readWorkspaceYml(workspacePath);
const entry = config.collections?.find((c) => c.name === 'SampleColl');
expect(entry).toBeDefined();
expect(entry?.remote).toBeUndefined();
});
await test.step('Verify .gitignore no longer contains the collection path', async () => {
expect(readGitignoreLines(workspacePath)).not.toContain(SAMPLE_COLL_GITIGNORE_LINE);
});
await closeElectronApp(app);
});
test('Connect to Git modal rejects empty and invalid URLs', async ({ launchElectronApp, createTmpDir }) => {
const workspacePath = await createTmpDir('git-ws-validation');
await copyFixture('workspace-with-collection', workspacePath);
const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } });
const page = await waitForReadyPage(app);
await switchWorkspace(page, FIXTURE_WS_NAME);
await test.step('Open Connect to Git modal', async () => {
await page.locator('.titlebar-left .home-button').click();
const card = page.locator('.collection-card').filter({ hasText: 'SampleColl' });
await card.locator('.collection-menu').click();
await page.locator('.dropdown-item').filter({ hasText: 'Connect to Git' }).click();
});
const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Connect to Git' });
await test.step('Empty URL should show validation error and keep modal open', async () => {
await modal.waitFor({ state: 'visible', timeout: 5000 });
await modal.locator('#remoteUrl').fill('');
await modal.getByRole('button', { name: 'Connect', exact: true }).click();
await expect(modal.locator('.text-red-500').first()).toBeVisible({ timeout: 2000 });
await expect(modal).toBeVisible();
});
await test.step('Malformed URL should also be rejected', async () => {
await modal.locator('#remoteUrl').fill('not-a-url');
await modal.getByRole('button', { name: 'Connect', exact: true }).click();
await expect(modal.locator('.text-red-500').first()).toBeVisible({ timeout: 2000 });
await expect(modal).toBeVisible();
});
await test.step('Valid URL submits successfully', async () => {
await modal.locator('#remoteUrl').fill(REMOTE_URL);
await modal.getByRole('button', { name: 'Connect', exact: true }).click();
await expect(page.getByText('Git remote connected')).toBeVisible({ timeout: 10000 });
await expect(modal).not.toBeVisible({ timeout: 5000 });
});
await closeElectronApp(app);
});
test('default workspace does not expose Git options on the collection card', async ({ launchElectronApp, createTmpDir }) => {
// No fixture: the playwright fixture default-seeds preferences to skip onboarding,
// and Bruno auto-creates the default workspace under the userData path.
const collectionDir = await createTmpDir('git-default-coll');
const app = await launchElectronApp();
const page = await waitForReadyPage(app);
await test.step('Verify we are on the default workspace', async () => {
await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 5000 });
});
await test.step('Create a collection in the default workspace', async () => {
await createCollection(page, 'DefaultColl', collectionDir);
});
await test.step('Open the collection menu in the workspace overview', async () => {
await page.locator('.titlebar-left .home-button').click();
const card = page.locator('.collection-card').filter({ hasText: 'DefaultColl' });
await card.waitFor({ state: 'visible', timeout: 5000 });
await card.locator('.collection-menu').click();
});
await test.step('No Git-related menu items should be visible', async () => {
await expect(page.locator('.dropdown-item').filter({ hasText: 'Connect to Git' })).toHaveCount(0);
await expect(page.locator('.dropdown-item').filter({ hasText: 'Copy Git URL' })).toHaveCount(0);
await expect(page.locator('.dropdown-item').filter({ hasText: 'Remove Git Remote' })).toHaveCount(0);
});
await closeElectronApp(app);
});
});
test.describe('Sidebar ghost row', () => {
test('git-backed entry whose folder is missing renders as a clickable ghost row', async ({ launchElectronApp, createTmpDir }) => {
const workspacePath = await createTmpDir('git-ws-ghost');
await copyFixture('workspace-with-ghost', workspacePath);
const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } });
const page = await waitForReadyPage(app);
await switchWorkspace(page, GHOST_WS_NAME);
const ghostRow = page.getByTestId('sidebar-git-collection-row').filter({ hasText: 'Missing Coll' });
await test.step('Ghost row appears in the sidebar', async () => {
await expect(ghostRow).toBeVisible({ timeout: 10000 });
});
await test.step('Ghost row is not also rendered as a normal sidebar collection row', async () => {
await expect(
page.getByTestId('sidebar-collection-row').filter({ hasText: 'Missing Coll' })
).toHaveCount(0);
});
await test.step('Clicking the ghost row opens the Clone Git Repository modal pre-filled with the remote URL', async () => {
await ghostRow.click();
const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: 'Clone' });
await cloneModal.waitFor({ state: 'visible', timeout: 5000 });
await expect(cloneModal).toContainText(REMOTE_URL);
});
await closeElectronApp(app);
});
test('right-clicking a ghost row exposes Remove Git Remote, which prunes both the entry and the row', async ({ launchElectronApp, createTmpDir }) => {
const workspacePath = await createTmpDir('git-ws-ghost-remove');
await copyFixture('workspace-with-ghost', workspacePath);
const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } });
const page = await waitForReadyPage(app);
await switchWorkspace(page, GHOST_WS_NAME);
const ghostRow = page.getByTestId('sidebar-git-collection-row').filter({ hasText: 'Missing Coll' });
await test.step('Wait for the ghost row to appear', async () => {
await expect(ghostRow).toBeVisible({ timeout: 10000 });
});
await test.step('Right-click the ghost row and choose Remove Git Remote', async () => {
await ghostRow.click({ button: 'right' });
await page.locator('.dropdown-item').filter({ hasText: 'Remove Git Remote' }).click();
const removeModal = page.locator('.bruno-modal-card').filter({ hasText: 'Remove Git Remote' });
await removeModal.waitFor({ state: 'visible', timeout: 5000 });
await removeModal.getByRole('button', { name: 'Remove', exact: true }).click();
await expect(page.getByText('Git remote removed')).toBeVisible({ timeout: 10000 });
});
await test.step('Ghost row disappears once the remote field is removed', async () => {
await expect(ghostRow).toHaveCount(0, { timeout: 5000 });
});
await test.step('workspace.yml entry persists but no longer has the remote field', async () => {
const config = readWorkspaceYml(workspacePath);
const entry = config.collections?.find((c) => c.name === 'Missing Coll');
expect(entry).toBeDefined();
expect(entry?.remote).toBeUndefined();
});
await closeElectronApp(app);
});
});
});