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
82 changes: 82 additions & 0 deletions frontend/e2e/specs/crop.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,85 @@ test.describe("crop", () => {
},
);
});

test.describe("PR #61 crop on a shared page", () => {
let before: Snapshot;

test.beforeEach(async ({ api, fixture }) => {
before = await takeSnapshot(api, fixture.review.sourceDocument);
});

test.afterEach(async ({ api, fixture }) => {
test.setTimeout(test.info().timeout + PROPAGATION_TIMEOUT);
await restoreCrops(api, fixture.review.sourceDocument, before);
});

test(
"W61-1 a crop on a page two sections share changes only the section holding the image",
{ tag: ["@functional", "@crop"] },
async ({ api, fixture }) => {
test.setTimeout(PROPAGATION_TIMEOUT + 120_000);
const sourceDocument = fixture.review.sourceDocument;
let target:
| {
pageNo: number;
tag: string;
caption: string;
holder: SectionSnapshot;
others: SectionSnapshot[];
}
| undefined;
for (const row of before.pages) {
const owners = owningSections(before.sections, row.page_no);
if (owners.length < 2) continue;
const figure = imageTags(row.canonical_markdown).find((tag) => !tag.caption.includes('"'));
if (!figure) continue;
const tag = `![${figure.caption}](${figure.url})`;
if (row.canonical_markdown.split(tag).length !== 2) continue;
const holders = owners.filter((owner) => owner.markdown.includes(tag));
if (holders.length !== 1 || holders[0].markdown.split(tag).length !== 2) continue;
target = {
pageNo: row.page_no,
tag,
caption: figure.caption,
holder: holders[0],
others: owners.filter((owner) => owner.name !== holders[0].name),
};
break;
}
test.skip(!target, "no page shared by two sections holds a figure in only one of them");
const { pageNo, tag, caption, holder, others } = target!;

const result = await api.call("wikify.api.pages.crop_page_figure", {
source_document: sourceDocument,
page_no: pageNo,
caption,
occurrence: 0,
x0: 0.25,
y0: 0.25,
x1: 0.75,
y1: 0.75,
});

const holderMarkdown = await waitFor(
() => api.getValue<string>("Source Section", holder.name, "markdown"),
(markdown) => markdown.includes(result.image_url),
{ timeout: 60_000, interval: 2_000, label: "holder section has the crop" },
);
expect(holderMarkdown).toBe(
holder.markdown.replace(tag, `![${caption}](${result.image_url})`),
);

await waitFor(
() => propagationPending(api, sourceDocument),
(pending) => !pending,
{ timeout: PROPAGATION_TIMEOUT, interval: 3_000, label: "propagation settled" },
);
for (const other of others) {
expect(await api.getValue<string>("Source Section", other.name, "markdown")).toBe(
other.markdown,
);
}
},
);
});
118 changes: 117 additions & 1 deletion frontend/e2e/specs/publish.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Page } from "@playwright/test";
import type { Api } from "../helpers/api";
import { serverMessage, type Api } from "../helpers/api";
import { PREFIX } from "../helpers/env";
import { expect, test } from "../helpers/test";
import { waitFor } from "../helpers/wait";
Expand Down Expand Up @@ -348,3 +348,119 @@ test.describe("publish", () => {
},
);
});

test.describe("PR #19 lock regenerate and hand off to the wiki app editor", () => {
test.describe.configure({ mode: "serial" });

const stamp = Date.now();
const name = `${PREFIX} publish W19 ${stamp}`;
const spaceRoute = `test-publish-w19-${stamp}`;
let importName = "";
let space = "";
let leaf: SectionRow;

test.beforeAll(async ({ api, fixture }) => {
test.setTimeout(QUEUE_TIMEOUT + 600_000);
const project = await createProject(api, name);
const cloned = await cloneImport(api, fixture.source.import, { title: name, project });
importName = cloned.import;
const { rows, children } = await outline(api, cloned.sourceDocument);
const leafName = children.find(
(child) => !rows.some((row) => row.parent_source_section === child.name),
)!.name;
await api.call("wikify.api.sections.build_graph", { import_name: importName });
await waitForImport(api, importName, "Graphed", 120_000);
await api.call("wikify.api.imports.generate_wiki", {
import_name: importName,
new_space: { space_name: name, route: spaceRoute },
});
await waitForImport(api, importName, "Completed", QUEUE_TIMEOUT);
space = (await api.getList("Wiki Space", { filters: { route: spaceRoute } }))[0].name;
leaf = (await sectionRows(api, cloned.sourceDocument)).find((row) => row.name === leafName)!;
});

test(
"W19-1 clicking a page after publish opens it in the Wiki app editor",
{ tag: ["@functional", "@publish"] },
async ({ page }) => {
await page.goto(`/wikify/import/${importName}/tree`);
await expect(page.getByText("Published", { exact: true })).toBeVisible();
const popupPromise = page.waitForEvent("popup");
await page
.locator(`[data-section-row="${leaf.name}"]`)
.getByText(leaf.title, { exact: true })
.click();
const popup = await popupPromise;
expect(new URL(popup.url()).pathname).toBe(
`/wiki-app/spaces/${space}/page/${leaf.wiki_document}`,
);
await popup.close();
},
);

test(
"W19-2 regenerating a published import is rejected",
{ tag: ["@negative", "@publish"] },
async ({ api }) => {
const before = await wikiDocuments(api, spaceRoute);
const response = await api.context.post("/api/method/wikify.api.imports.generate_wiki", {
data: { import_name: importName, wiki_space: space },
});
expect(response.status()).toBe(417);
expect(serverMessage(await response.json())).toContain(
"This wiki has already been published — edit pages directly in the Wiki app.",
);
expect(await api.getValue("Wikify Import", importName, "status")).toBe("Completed");
expect(await wikiDocuments(api, spaceRoute)).toEqual(before);
},
);
});

test.describe("PR #18 collapse tabs to pdf/pages/wiki/logs", () => {
test.describe.configure({ mode: "serial" });

const name = `${PREFIX} publish W18 ${Date.now()}`;
let importName = "";
let sourceDocument = "";

test.beforeAll(async ({ api, fixture }) => {
test.setTimeout(900_000);
const project = await createProject(api, name);
const cloned = await cloneImport(api, fixture.source.import, { title: name, project });
importName = cloned.import;
sourceDocument = cloned.sourceDocument;
await api.call("wikify.api.sections.build_graph", { import_name: importName });
await waitForImport(api, importName, "Graphed", 120_000);
});

test(
"W18-1 the old wiki link lands on the merged Wiki tab with Publish",
{ tag: ["@functional", "@publish"] },
async ({ page }) => {
await page.goto(`/wikify/import/${importName}/wiki`);
await expect(page).toHaveURL(new RegExp(`/wikify/import/${importName}/tree$`));
for (const label of ["PDF", "Pages", "Wiki", "Logs"])
await expect(page.getByRole("tab", { name: label, exact: true })).toBeVisible();
await expect(page.getByRole("tab", { name: "Explore", exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Publish", exact: true })).toBeEnabled();
},
);

test(
"W18-2 the publish dialog refuses to generate when no section is included",
{ tag: ["@negative", "@publish"] },
async ({ page, api }) => {
const rows = await sectionRows(api, sourceDocument);
for (const row of rows.filter((r) => r.include_in_wiki && !r.parent_source_section))
await api.call("wikify.api.sections.toggle_include", { name: row.name, include: 0 });
await waitFor(
async () => (await sectionRows(api, sourceDocument)).filter((r) => r.include_in_wiki),
(included) => included.length === 0,
{ timeout: 60_000, interval: 2_000, label: "all sections excluded" },
);
const dialog = await openPublishDialog(page, importName);
await expect(dialog.getByText(/nothing to publish/)).toBeVisible();
await expect(dialog.getByRole("button", { name: "Generate wiki" })).toBeDisabled();
},
);
});
74 changes: 74 additions & 0 deletions frontend/e2e/specs/settings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Page } from "@playwright/test";
import { expect, test } from "../helpers/test";

const SINGLE = "Wikify Settings";

async function openModels(page: Page): Promise<boolean> {
await page.goto("/wikify/settings/models");
await expect(page.getByText("OpenRouter model ids per pipeline step.")).toBeVisible();
return page.evaluate(() => !!(window as any).developer_mode);
}

test.describe("PR #57 local models toggle", () => {
let original = 0;

test.beforeEach(async ({ api }) => {
original = Number(await api.getValue(SINGLE, SINGLE, "use_local_models")) || 0;
});

test.afterEach(async ({ api }) => {
await api.setValue(SINGLE, SINGLE, { use_local_models: original });
});

test("W57-1 the Local models switch saves use_local_models in developer mode @functional @settings", async ({
page,
api,
}) => {
const developerMode = await openModels(page);
test.skip(!developerMode, "site is not in developer mode");

await api.setValue(SINGLE, SINGLE, { use_local_models: 0 });
await page.reload();
await expect(page.getByText("Local models", { exact: true })).toBeVisible();

const toggle = page.getByRole("switch").first();
await toggle.click();
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Settings saved")).toBeVisible();
expect(Number(await api.getValue(SINGLE, SINGLE, "use_local_models"))).toBe(1);
});

test("W57-2 the Local models switch is hidden outside developer mode @negative @settings", async ({
page,
}) => {
const developerMode = await openModels(page);
test.skip(developerMode, "site is in developer mode");

await expect(page.getByText("Agent model")).toBeVisible();
await expect(page.getByText("Local models", { exact: true })).toHaveCount(0);
});
});

test.describe("PR #60 settings dialog in the url", () => {
test("W60-1 a settings link opens on its tab, tabs update the url and Escape returns to the page @functional @settings", async ({
page,
}) => {
await page.goto("/wikify/settings/models");
await expect(page.getByText("OpenRouter model ids per pipeline step.")).toBeVisible();

await page.getByRole("tab", { name: "Scoring" }).click();
await expect(page).toHaveURL(/\/wikify\/settings\/scoring$/);

await page.keyboard.press("Escape");
await expect(page).not.toHaveURL(/\/settings\//);
await expect(page.getByRole("tab", { name: "Scoring" })).toHaveCount(0);
});

test("W60-2 an unknown settings tab falls back to the first tab @negative @settings", async ({
page,
}) => {
await page.goto("/wikify/settings/nonsense");
await expect(page).toHaveURL(/\/wikify\/settings\/openrouter$/);
await expect(page.getByText("OpenRouter model ids per pipeline step.")).toBeHidden();
});
});
Loading