@@ -0,0 +1,165 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The document-review flow in a browser (plan item AT2.3): upload a PDF, the worker reads it through the
|
||||
// fake platform's OCR job API, the four labelled lines of the fixture become proposals (its drawing page
|
||||
// is reported as not read), a person approves, edits and rejects, and the Excel export holds the approved
|
||||
// lines and only those.
|
||||
|
||||
import { createHash, randomBytes } from "node:crypto"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { sheetRows } from "../modules/documents/__tests__/support/unzip"
|
||||
import { moduleSelected } from "../modules/inclusion"
|
||||
import { call, makeOrganization, signIn, unique, violations } from "./support/app"
|
||||
|
||||
test.skip(!moduleSelected("documents"), "Documents must be included in this isolated test configuration")
|
||||
|
||||
const PDF = Buffer.from("%PDF-1.4\n1 0 obj<<>>endobj\ntrailer<<>>\n%%EOF\n")
|
||||
|
||||
test("a fixture PDF becomes lines a person reviews, and only approved lines are exported", async ({ page }) => {
|
||||
await signIn(page, `${unique("reviewer")}@example.test`)
|
||||
const { org, workspace, project } = await makeOrganization(page)
|
||||
const base = `/${org}/${workspace}/${project}/documents`
|
||||
|
||||
await page.goto(base)
|
||||
await expect(page.getByRole("heading", { level: 1, name: "Documents" })).toBeVisible()
|
||||
expect(await violations(page)).toEqual([])
|
||||
|
||||
await page.getByRole("button", { name: "Add a document", exact: true }).click()
|
||||
// upload through the file input of the form: the request answers before any OCR has run
|
||||
await page
|
||||
.getByLabel("Upload a document")
|
||||
.setInputFiles({ name: "report.pdf", mimeType: "application/pdf", buffer: PDF })
|
||||
await page.getByRole("button", { name: "Upload", exact: true }).click()
|
||||
// an upload is three requests (begin, the piece, complete); once the last has answered the live list
|
||||
// reads again, with the file in it: wait for that, or the navigation below would cut the upload off
|
||||
await expect(page.getByRole("row", { name: /report\.pdf/ })).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
// the worker picks the file up, submits it to the OCR job API and proposes the lines; the list follows
|
||||
// through the live stream, with no reload and no navigation (a reload would lose this marker)
|
||||
await page.evaluate(() => {
|
||||
;(window as unknown as { conusaiSamePage?: boolean }).conusaiSamePage = true
|
||||
})
|
||||
await expect(page.getByRole("row").filter({ hasText: /report\.pdf.*Ready for review/ })).toBeVisible({
|
||||
timeout: 60_000,
|
||||
})
|
||||
expect(await page.evaluate(() => (window as unknown as { conusaiSamePage?: boolean }).conusaiSamePage)).toBe(true)
|
||||
|
||||
await page.getByRole("link", { name: "Review report.pdf" }).click()
|
||||
await expect(page.getByRole("heading", { level: 1 })).toContainText("report.pdf")
|
||||
// what was proposed by a machine says so, in text
|
||||
await expect(page.getByText("AI-generated content")).toBeVisible()
|
||||
await expect(page.getByRole("row")).toHaveCount(5) // header and four lines
|
||||
// the fixture's third page is a drawing: the extraction left it out, and the page says so
|
||||
await expect(page.getByRole("heading", { name: "Pages not read" })).toBeVisible()
|
||||
await expect(page.getByText("Page 3: drawing. It was left out of the extraction.")).toBeVisible()
|
||||
await expect(page.getByRole("button", { name: "Export approved lines (Excel)" })).toBeDisabled()
|
||||
expect(await violations(page)).toEqual([])
|
||||
|
||||
// each line shows where it was read
|
||||
await expect(page.getByRole("complementary", { name: "Source, page 2" }).first()).toContainText(
|
||||
"Total amount: 1 250,00 EUR",
|
||||
)
|
||||
|
||||
// approve one, correct another, reject a third
|
||||
await page.getByRole("button", { name: "Approve Reference" }).click()
|
||||
await expect(page.getByRole("row", { name: /Reference.*Approved/ })).toBeVisible()
|
||||
|
||||
await page.getByRole("button", { name: "Edit Issued on" }).click()
|
||||
await page.getByLabel("Label").fill("Issue date")
|
||||
await page.getByLabel("Value").fill("2026-04-02")
|
||||
await page.getByRole("button", { name: "Save" }).click()
|
||||
await expect(page.getByRole("row", { name: /Issue date.*Edited/ })).toBeVisible()
|
||||
await page.getByRole("button", { name: "Approve Issue date" }).click()
|
||||
await expect(page.getByRole("row", { name: /Issue date.*Approved/ })).toBeVisible()
|
||||
|
||||
await page.getByRole("button", { name: "Reject Signed by" }).click()
|
||||
await expect(page.getByRole("row", { name: /Signed by.*Rejected/ })).toBeVisible()
|
||||
// the header's counts follow the decisions on the same page
|
||||
await expect(page.getByText("1 line to review. 2 lines approved.")).toBeVisible()
|
||||
// the line nobody decided is still waiting, and is not exported
|
||||
await expect(page.getByRole("row", { name: /Total amount.*To review/ })).toBeVisible()
|
||||
|
||||
const download = page.waitForEvent("download")
|
||||
await page.getByRole("button", { name: "Export approved lines (Excel)" }).click()
|
||||
const file = await download
|
||||
expect(file.suggestedFilename()).toBe("report-approved.xlsx")
|
||||
const path = await file.path()
|
||||
const rows = sheetRows(new Uint8Array(readFileSync(path)))
|
||||
expect(rows).toEqual([
|
||||
["Label", "Value", "Page", "Source quote", "Edited by a reviewer"],
|
||||
["Reference", "QR-2026-04", 1, "Reference: QR-2026-04", "no"],
|
||||
["Issue date", "2026-04-02", 1, "Issued on: 2026-04-01", "yes"],
|
||||
])
|
||||
expect(await violations(page)).toEqual([])
|
||||
})
|
||||
|
||||
test("a 30 MB PDF is uploaded in the browser with a progress bar, stored whole, and digested by the platform", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await signIn(page, `${unique("uploader")}@example.test`)
|
||||
const { org, workspace, project } = await makeOrganization(page)
|
||||
const base = `/${org}/${workspace}/${project}/documents`
|
||||
await page.goto(base)
|
||||
await page.getByRole("button", { name: "Add a document", exact: true }).click()
|
||||
|
||||
// 30 MB that start like a PDF and are not all one byte, so a piece that arrives short or twice
|
||||
// cannot hash to the same digest. The server entry passes such a body through unread (src/server.ts), so a
|
||||
// step that buffered or cut it would show here.
|
||||
const size = 30 * 1024 * 1024
|
||||
const buffer = Buffer.concat([Buffer.from("%PDF-1.4\n"), randomBytes(size - 9)])
|
||||
expect(buffer.length).toBe(size)
|
||||
|
||||
// Slow the upload to 8 MB/s, so that the bar is on screen for seconds and not for a blink: what
|
||||
// the person sees is the requirement, and a loopback upload would be over before a poll looked.
|
||||
const cdp = await page.context().newCDPSession(page)
|
||||
await cdp.send("Network.enable")
|
||||
await cdp.send("Network.emulateNetworkConditions", {
|
||||
offline: false,
|
||||
latency: 0,
|
||||
downloadThroughput: -1,
|
||||
uploadThroughput: 8 * 1024 * 1024,
|
||||
})
|
||||
await page.getByLabel("Upload a document").setInputFiles({ name: "tender.pdf", mimeType: "application/pdf", buffer })
|
||||
await page.getByRole("button", { name: "Upload", exact: true }).click()
|
||||
|
||||
// the bar names the file and how far it has got, and the live list shows the file when the upload is done
|
||||
await expect(page.getByRole("progressbar", { name: /Uploading tender\.pdf: \d+%/ })).toBeVisible()
|
||||
await page.locator('[data-slot="side-panel"]').getByRole("button", { name: /Close/ }).click()
|
||||
await expect(page.getByRole("progressbar", { name: /Uploading tender\.pdf: \d+%/ })).toBeVisible()
|
||||
await page.screenshot({ path: testInfo.outputPath("document-upload-pending.png") })
|
||||
await expect(page.getByRole("row", { name: /tender\.pdf/ })).toBeVisible({ timeout: 60_000 })
|
||||
|
||||
await cdp.send("Network.emulateNetworkConditions", {
|
||||
offline: false,
|
||||
latency: 0,
|
||||
downloadThroughput: -1,
|
||||
uploadThroughput: -1,
|
||||
})
|
||||
|
||||
const listed = await call<{ files: Array<{ filename: string; size: number; sha256: string }> }>(
|
||||
page,
|
||||
"files.file.list",
|
||||
{
|
||||
org,
|
||||
workspace,
|
||||
project,
|
||||
},
|
||||
)
|
||||
const stored = listed.files.find((file) => file.filename === "tender.pdf")
|
||||
expect(stored?.size).toBe(size)
|
||||
expect(stored?.sha256).toBe(createHash("sha256").update(buffer).digest("hex"))
|
||||
})
|
||||
|
||||
test("another organization's member does not see the document", async ({ page, browser }) => {
|
||||
await signIn(page, `${unique("owner")}@example.test`)
|
||||
const mine = await makeOrganization(page)
|
||||
const stranger = await browser.newPage()
|
||||
await signIn(stranger, `${unique("stranger")}@example.test`)
|
||||
const response = await stranger.goto(`/${mine.org}/${mine.workspace}/${mine.project}/documents`)
|
||||
// the organization layout answers a non-member as it answers a stranger: the not-found page, as a
|
||||
// 404, with no document list and no names
|
||||
expect(response?.status()).toBe(404)
|
||||
await expect(stranger.getByRole("heading", { level: 1, name: "Page not found" })).toBeVisible()
|
||||
expect(await stranger.content()).not.toContain(`${mine.org} Review`)
|
||||
await stranger.close()
|
||||
})
|
||||
Reference in New Issue
Block a user