All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m30s
CI / Build container images (pull_request) Successful in 1m21s
CI / Auth e2e pack (pull_request) Successful in 8m49s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 35s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Deploy to Test (push) Successful in 14s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m49s
CI / Import/export fidelity gate (push) Successful in 1m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m30s
The Obsidian fixture vault contains a note called "Startseite", and the pond now creates one too — the seeded fixtures use locale `de`. Two consequences, and the second is the one that mattered: - the unscoped title locator matched two sidebar entries; - `/p/<pond>/startseite` no longer belongs to the imported note. The pond's own start page took that slug, so the import landed on a suffixed one and the test was about to assert against the wrong page. Both are fixed by scoping to the mount page and navigating through the sidebar instead of guessing a slug. The test stays meaningful: it then clicks a wikilink inside the page content, which the empty auto-created start page would not have. CI caught this; the local run passed it. Worth remembering that a title-based locator can go green by luck.
144 lines
5.8 KiB
TypeScript
144 lines
5.8 KiB
TypeScript
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
import { dirname, join, relative } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { expect, test, type BrowserContext } from '@playwright/test';
|
|
import { zipSync } from 'fflate';
|
|
|
|
import { contextForUser } from './helpers';
|
|
|
|
/**
|
|
* Obsidian vault import pack (issues #117/#118): a pond admin imports the
|
|
* checked-in fixture vault through the settings dialog and the result shows
|
|
* up in the app — folder tree, rewritten wikilinks, tag labels, embedded
|
|
* image. A plain editor sees no such section. The ZIP is built from the
|
|
* fixture directory here, so the vault stays reviewable as plain files.
|
|
*/
|
|
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
|
const FIXTURE_DIR = join(
|
|
dirname(fileURLToPath(import.meta.url)),
|
|
'../../../fixtures/import/obsidian-vault',
|
|
);
|
|
|
|
function fixtureZip(): Buffer {
|
|
const entries: Record<string, Uint8Array> = {};
|
|
const walk = (dir: string): void => {
|
|
for (const name of readdirSync(dir)) {
|
|
const path = join(dir, name);
|
|
if (statSync(path).isDirectory()) walk(path);
|
|
else entries[relative(FIXTURE_DIR, path)] = new Uint8Array(readFileSync(path));
|
|
}
|
|
};
|
|
walk(FIXTURE_DIR);
|
|
return Buffer.from(zipSync(entries));
|
|
}
|
|
|
|
async function json<T>(context: BrowserContext, url: string, data: unknown): Promise<T> {
|
|
const response = await context.request.post(url, { data });
|
|
if (!response.ok()) throw new Error(`post ${url} → ${response.status()}`);
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
test('a pond admin imports an Obsidian vault through the settings dialog', async ({ browser }) => {
|
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const ts = Date.now();
|
|
const pond = await json<{ id: string; slug: string }>(context, '/api/v1/ponds', {
|
|
name: `Vault Pond ${ts}`,
|
|
});
|
|
await json<{ id: string; slug: string; title: string }>(
|
|
context,
|
|
`/api/v1/ponds/${pond.id}/pages`,
|
|
{ title: `Vault Mount ${ts}` },
|
|
);
|
|
const labelName = `import-${ts}`;
|
|
|
|
const page = await context.newPage();
|
|
await page.goto(`/p/${pond.slug}/settings`);
|
|
|
|
// The admin-only section opens the dialog.
|
|
await page.locator('.vault-import__open').click();
|
|
const dialog = page.locator('.vault-import-dialog');
|
|
await expect(dialog).toBeVisible();
|
|
// #120: the panel must be opaque — --color-surface was undefined and the
|
|
// whole .modal background silently dropped, letting the page bleed through.
|
|
await expect(dialog).toHaveCSS('background-color', 'rgb(255, 255, 255)');
|
|
|
|
await dialog.locator('input[type="file"]').setInputFiles({
|
|
name: 'vault.zip',
|
|
mimeType: 'application/zip',
|
|
buffer: fixtureZip(),
|
|
});
|
|
// Mount under the prepared page. The pond has no labels yet — the label
|
|
// section must still be there (#121) and let the admin create one inline;
|
|
// the fresh label comes back pre-ticked.
|
|
await dialog
|
|
.locator('li', { hasText: `Vault Mount ${ts}` })
|
|
.locator('input')
|
|
.check();
|
|
await dialog.locator('.vault-import-dialog__label-create input').fill(labelName);
|
|
await dialog.locator('.vault-import-dialog__label-create-submit').click();
|
|
await expect(dialog.locator('li', { hasText: labelName }).locator('input')).toBeChecked();
|
|
await dialog.locator('.vault-import-dialog__start').click();
|
|
|
|
// The job runs; the dialog offers the way into the imported pages.
|
|
const doneLink = dialog.getByRole('link');
|
|
await expect(doneLink).toBeVisible({ timeout: 30_000 });
|
|
await doneLink.click();
|
|
|
|
// The sidebar shows the vault's folder structure under the mount page.
|
|
const mountItem = page
|
|
.locator('.sidebar__page-item')
|
|
.filter({ has: page.locator(`.sidebar__page:text-is("Vault Mount ${ts}")`) })
|
|
.first();
|
|
await expect(
|
|
mountItem.locator('.sidebar__tree-children .sidebar__page', { hasText: 'Projekte' }),
|
|
).toBeVisible();
|
|
// Scoped to the mount: the pond has its own "Startseite" since issue #302,
|
|
// so an unscoped title match now finds two entries.
|
|
const importedHome = mountItem
|
|
.locator('.sidebar__tree-children .sidebar__page')
|
|
.filter({ hasText: /^Startseite$/ })
|
|
.first();
|
|
await expect(importedHome).toBeVisible();
|
|
|
|
// A rewritten Obsidian link navigates to the right imported page, and the
|
|
// display text still reads like the original note name. Reached through the
|
|
// sidebar rather than by slug — `/startseite` belongs to the pond's own
|
|
// start page, so the imported note landed on a suffixed slug.
|
|
await importedHome.click();
|
|
await page.locator('.editor-content a.wikilink', { hasText: 'Projekt A' }).click();
|
|
await expect(page).toHaveURL(new RegExp(`/p/${pond.slug}/projekt-a$`));
|
|
|
|
// The note's image came in as a pond file, and its tags became labels
|
|
// (nested #status/aktiv included) next to the dialog's extra label.
|
|
await expect(page.locator('.editor-content img').first()).toBeVisible();
|
|
const labels = await context.request.get(`/api/v1/ponds/${pond.id}/labels`);
|
|
const tree = JSON.stringify(await labels.json());
|
|
expect(tree).toContain('status');
|
|
expect(tree).toContain('aktiv');
|
|
expect(tree).toContain(labelName);
|
|
|
|
await context.close();
|
|
});
|
|
|
|
test('a plain editor gets no vault-import section', async ({ browser }) => {
|
|
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const editor = await contextForUser(browser, BASE_URL, 'fixture-editor');
|
|
const ts = Date.now();
|
|
const pond = await json<{ id: string; slug: string }>(owner, '/api/v1/ponds', {
|
|
name: `Vault Gate ${ts}`,
|
|
});
|
|
await json(owner, `/api/v1/ponds/${pond.id}/members`, {
|
|
usernameOrEmail: 'fixture-editor',
|
|
role: 'editor',
|
|
});
|
|
|
|
const page = await editor.newPage();
|
|
await page.goto(`/p/${pond.slug}/settings`);
|
|
await expect(page.locator('.pond-settings-page')).toBeVisible();
|
|
await expect(page.locator('.vault-import__open')).toHaveCount(0);
|
|
|
|
await owner.close();
|
|
await editor.close();
|
|
});
|