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: 2 additions & 0 deletions web/stovepipe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ return to the first page to see newly recorded projects.

The request page shows a compact summary with change and base URIs always visible. Project results are the default view; History is a separate tab at `?view=history`. Build and Test Issues (`?view=issues`) and Fix Attempts (`?view=fix-attempts`) show Coming soon. Switching tabs preserves the project cursor. Project results and history tables fill the remaining page height and scroll within the active tab. On small screens, a minimum table height keeps rows usable while the page can scroll to reach the controls. Project pagination controls appear above and below the table, show the number of projects on the page, and use the host router without a document reload or resetting the document scroll position. A new project page starts at the top of the table, while the surrounding page and pagination controls stay in place, including on a short final page. Host router links should honor `WebLinkProps.scroll` to support this behavior. Browser Back and Forward restore the selected tab and cursor; links also work without JavaScript. The repository result appears in the summary above all tabs; the project results table contains only projects. Recorded results show Green at zero, Partial Breakage between zero and one, and Breakage at one, without percentages.

Queue rows and request summaries show recorded broken projects out of the total when the total is positive, including zero broken projects. A zero total displays a dash. Counts come from the backend summary, not the current page of project results, so pagination does not change the ratio.

Repository breakage, per-project breakage, and recording completion are separate.
Zero breakage is green; absent results are never treated as green. Completion
describes result production, not whether all pages have been displayed. Project
Expand Down
2 changes: 2 additions & 0 deletions web/stovepipe/controller/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ export function mapRequestSummary(
updatedAtMs: request.stateUpdatedAtMs.toString(),
acceptedAtMs: request.acceptedAtMs.toString(),
outcomeReason: request.outcomeReason,
projectCount: request.projectCount,
brokenProjectCount: request.brokenProjectCount,
};
}
4 changes: 4 additions & 0 deletions web/stovepipe/entity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export interface RequestSummaryModel {
acceptedAtMs: string;
/** Stable backend outcome reason, empty when absent. */
outcomeReason: string;
/** Total recorded project results; zero means no ratio to display. */
projectCount: number;
/** Recorded project results with nonzero breakage. */
brokenProjectCount: number;
}
export interface QueueRequestsModel {
/** Requests on this page, newest accepted first. */
Expand Down
2 changes: 2 additions & 0 deletions web/stovepipe/extension/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface RequestSummary {
stateUpdatedAtMs: bigint;
acceptedAtMs: bigint;
outcomeReason: string;
projectCount: number;
brokenProjectCount: number;
}

export interface HistoryEvent {
Expand Down
8 changes: 8 additions & 0 deletions web/stovepipe/module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const summary: RequestSummary = {
stateUpdatedAtMs: 9007199254740993n,
acceptedAtMs: 100n,
outcomeReason: "future_reason",
projectCount: 51,
brokenProjectCount: 2,
};
function createPageTestContext() {
const service: StovepipeService = {
Expand Down Expand Up @@ -157,6 +159,10 @@ describe("Stovepipe page loading", () => {
]);
expect(service.getRequestHistoryByID).not.toHaveBeenCalled();
expect(service.getProjectStatusByID).not.toHaveBeenCalled();
expect(result.model.requests.data.requests).toEqual([
expect.objectContaining({ projectCount: 51, brokenProjectCount: 2 }),
expect.objectContaining({ projectCount: 51, brokenProjectCount: 2 }),
]);
});
it("loads the current summary and events by ID without reconstructing state from history", async () => {
const { app, service, paths } = createPageTestContext();
Expand Down Expand Up @@ -190,6 +196,8 @@ describe("Stovepipe page loading", () => {
updatedAtMs: "9007199254740993",
acceptedAtMs: "100",
outcomeReason: "future_reason",
projectCount: 51,
brokenProjectCount: 2,
},
events: [{ label: "future_event", outcomeReason: "event_reason" }],
},
Expand Down
7 changes: 7 additions & 0 deletions web/stovepipe/view/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const model: StovepipePageModel = {
updatedAtMs: "1000",
acceptedAtMs: "100",
outcomeReason: "all_targets_passed",
projectCount: 51,
brokenProjectCount: 2,
},
events: [
{
Expand All @@ -65,6 +67,7 @@ it("displays request summary data", () => {
expect(within(status).getByText("git://repo/sha")).toBeVisible();
expect(within(status).getByText("git://repo/base")).toBeVisible();
expect(within(status).getByText("all_targets_passed")).toBeVisible();
expect(within(status).getByText("2 of 51")).toBeVisible();
expect(within(status).getByText("Accepted")).toBeVisible();
expect(within(status).getByText("Updated")).toBeVisible();
expect(within(status).getByText("Repository result")).toBeVisible();
Expand Down Expand Up @@ -104,6 +107,7 @@ it("retains the summary and history after a failed refresh", async () => {
);
expect(screen.getByText("git://repo/sha")).toBeVisible();
expect(screen.getByText("all_targets_passed")).toBeVisible();
expect(screen.getByText("2 of 51")).toBeVisible();
expect(screen.getByText("accepted")).toBeInTheDocument();
await waitFor(() => expect(screen.getAllByText("Showing the last successful result.")).toHaveLength(2));
expect(screen.getByText("green-project")).toBeVisible();
Expand All @@ -124,6 +128,7 @@ it("discards previous results when the request changes", () => {
expect(screen.getByRole("heading", { name: "Request 8" })).toBeVisible();
expect(screen.queryByText("git://repo/sha")).toBeNull();
expect(screen.queryByText("all_targets_passed")).toBeNull();
expect(screen.queryByText("2 of 51")).toBeNull();
expect(screen.queryByText("green-project")).toBeNull();
});

Expand All @@ -138,6 +143,8 @@ it("renders recorded project results, completion and shareable pagination", () =
expect(within(projects).getByText("Breakage")).toBeVisible();
expect(within(projects).getByText("No result recorded")).toBeVisible();
expect(within(projects).queryByText(/%/)).toBeNull();
expect(within(screen.getByRole("region", { name: "Request status" }))
.getByText("2 of 51")).toBeVisible();
expect(within(projects).getByText(/still in progress/)).toBeVisible();
for (const link of within(projects).getAllByRole("link", { name: /Next projects/ })) {
expect(link).toHaveAttribute("href", "/repo%2Fmain/request/7?projects=opaque%2B%2F%3D");
Expand Down
56 changes: 56 additions & 0 deletions web/stovepipe/view/pages.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { cleanup, render, screen, within } from "../../platform/test/rtl.js";
import { afterEach, expect, it } from "vitest";
import type { RequestSummaryModel } from "../entity/index.js";
import { RequestDetails, RequestList } from "./pages.js";
import { QueueRequests } from "./queue.js";

afterEach(cleanup);

const request: RequestSummaryModel = {
id: "7",
href: "/queue/request/7",
changeUri: "git://repo/change",
baseUri: "",
state: "failed",
acceptedAtMs: "1000",
updatedAtMs: "2000",
outcomeReason: "build_failed",
projectCount: 0,
brokenProjectCount: 0,
};

it.each([
{ name: "no recorded projects", total: 0, broken: 0, display: "—" },
{ name: "all green", total: 51, broken: 0, display: "0 of 51" },
{ name: "mixed outcomes", total: 51, broken: 2, display: "2 of 51" },
{ name: "all broken", total: 51, broken: 51, display: "51 of 51" },
])("shows $name in the queue and request summary", ({ total, broken, display }) => {
const summary = { ...request, projectCount: total, brokenProjectCount: broken };
render(
<>
<RequestList requests={[summary]} />
<section aria-label="Summary"><RequestDetails request={summary} repositoryResult={null} /></section>
</>,
);
const table = within(screen.getByRole("table", { name: "Requests" }));
expect(table.getByRole("columnheader", { name: "Broken projects" })).toBeVisible();
expect(table.getByRole("cell", { name: display })).toBeVisible();
const details = within(screen.getByRole("region", { name: "Summary" }));
expect(details.getByText("Broken projects")).toBeVisible();
expect(details.getByText(display, { exact: true })).toBeVisible();
});

it("refreshes counts on an already displayed queue row", () => {
const { rerender } = render(
<QueueRequests routeKey="queue" initial={{ requests: [request], nextHref: null }} />,
);
const table = within(screen.getByRole("table", { name: "Requests" }));
expect(table.getByRole("cell", { name: "—" })).toBeVisible();
rerender(
<QueueRequests routeKey="queue" initial={{
requests: [{ ...request, projectCount: 3, brokenProjectCount: 1 }],
nextHref: null,
}} />,
);
expect(table.getByRole("cell", { name: "1 of 3" })).toBeVisible();
});
11 changes: 10 additions & 1 deletion web/stovepipe/view/pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ function RequestTimestamp({ value }: { value: string }) {
return <Timestamp value={value} />;
}

function formatBrokenProjectCounts(request: RequestSummaryModel) {
return request.projectCount > 0
? `${request.brokenProjectCount} of ${request.projectCount}`
: "—";
}

export function RequestList({
requests,
}: {
Expand All @@ -37,6 +43,7 @@ export function RequestList({
"Accepted",
"Updated",
"Outcome",
"Broken projects",
]}
>
{requests.map((request) => (
Expand All @@ -59,6 +66,7 @@ export function RequestList({
<RequestTimestamp value={request.updatedAtMs} />
</StyledTableBodyCell>
<StyledTableBodyCell>{request.outcomeReason || "—"}</StyledTableBodyCell>
<StyledTableBodyCell>{formatBrokenProjectCounts(request)}</StyledTableBodyCell>
</StyledTableBodyRow>
))}
</Table>
Expand All @@ -68,7 +76,7 @@ export function RequestList({
export function RequestDetails({ request, repositoryResult }: { request: RequestSummaryModel | null; repositoryResult: ReactNode }) {
return (
<>
<Block as="dl" margin="0" display="grid" gridTemplateColumns={["minmax(0, 1fr)", "repeat(2, minmax(0, 1fr))", "repeat(3, minmax(0, 1fr))", "repeat(5, minmax(0, 1fr))"]} gridGap="scale400">
<Block as="dl" margin="0" display="grid" gridTemplateColumns={["minmax(0, 1fr)", "repeat(2, minmax(0, 1fr))", "repeat(3, minmax(0, 1fr))", "repeat(6, minmax(0, 1fr))"]} gridGap="scale400">
{request && <Fact label="State" compact>
<Tag closeable={false} kind="neutral" size="small">
{request.state || "Unknown"}
Expand All @@ -79,6 +87,7 @@ export function RequestDetails({ request, repositoryResult }: { request: Request
<Fact label="Accepted" compact><RequestTimestamp value={request.acceptedAtMs} /></Fact>
<Fact label="Updated" compact><RequestTimestamp value={request.updatedAtMs} /></Fact>
<Fact label="Outcome" compact>{request.outcomeReason || "—"}</Fact>
<Fact label="Broken projects" compact>{formatBrokenProjectCounts(request)}</Fact>
</>}
</Block>
{request && <Block as="dl" marginTop="scale300" marginBottom="scale400" display="grid" gridTemplateColumns={["minmax(0, 1fr)", "repeat(2, minmax(0, 1fr))"]} gridGap="scale400">
Expand Down
5 changes: 3 additions & 2 deletions web/test/e2e/stovepipe/run_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ def _pagination_fixture_sql() -> str:
"?file=src%2Fmain#fragment" if i == 51 else ""
)
state = "failed" if i == 51 else "succeeded"
project_count, broken_project_count = (51, 2) if i == 51 else (0, 0)
statements.append(
"INSERT INTO request_summary "
"(queue,request_id,uri,base_uri,state,request_version,"
"state_timestamp_ms,version,accepted_at_ms,outcome_reason) "
f"VALUES ('web/pagination','{i}','{uri}','','{state}',1,{1000+i},1,{1000+i},'');"
"state_timestamp_ms,version,accepted_at_ms,outcome_reason,project_count,broken_project_count) "
f"VALUES ('web/pagination','{i}','{uri}','','{state}',1,{1000+i},1,{1000+i},'',{project_count},{broken_project_count});"
)
statements.append(
f"INSERT INTO request_acceptance VALUES ('web/pagination',{1000+i},'{i}');"
Expand Down
5 changes: 5 additions & 0 deletions web/test/e2e/stovepipe/stovepipe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ test.describe("server-rendered styling", () => {
test("renders request status and views without JavaScript", async ({ page }) => {
await page.goto("/web%2Fpagination/request/51");
const status = page.getByRole("region", { name: "Request status" });
await expect(status.getByText("Broken projects", { exact: true })).toBeVisible();
await expect(status.getByText("2 of 51", { exact: true })).toBeVisible();
await expect(status.getByText("Repository result", { exact: true })).toBeVisible();
await expect(status.getByText("Partial Breakage", { exact: true })).toBeVisible();
await expect(status.getByText("git://web/pagination/51?file=src%2Fmain#fragment", { exact: true })).toBeVisible();
Expand Down Expand Up @@ -280,6 +282,8 @@ test("real ingestion, queue selection, cursors, request summaries, history, proj
.getByRole("link", { name: "web/pagination", exact: true })
.click();
await expect(requests.locator("tbody tr")).toHaveCount(50);
await expect(requests.getByRole("columnheader", { name: "Broken projects", exact: true })).toBeVisible();
await expect(requests.getByText("2 of 51", { exact: true })).toBeVisible();
await page.screenshot({
path: `${process.env.TEST_UNDECLARED_OUTPUTS_DIR}/stovepipe-queue.png`,
fullPage: true,
Expand Down Expand Up @@ -310,6 +314,7 @@ test("real ingestion, queue selection, cursors, request summaries, history, proj
).toBeVisible();
await expect(status.getByText("Accepted", { exact: true })).toBeVisible();
await expect(status.getByText("Updated", { exact: true })).toBeVisible();
await expect(status.getByText("2 of 51", { exact: true })).toBeVisible();
await page.getByRole("navigation", { name: "Request views" }).getByRole("link", { name: "History", exact: true }).click();
await expect(
page.getByRole("table", { name: "Request history" }).getByText("accepted", {
Expand Down
2 changes: 1 addition & 1 deletion web/test/package/react18/react18-runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ test("renders and hydrates packaged pages with React 18 and host themes", async
projectResultsComplete: true, nextPageToken: "",
projects: [{ project: "example-project", result: { case: "breakageDegree", value: 0 } }],
}),
getRequestHistoryByID: async () => ({ requestSummary: { requestId: "1", queue: "repo/main", changeUri: "git://repo/main/sha", baseUri: "", requestState: "accepted", stateUpdatedAtMs: 100n, acceptedAtMs: 100n, outcomeReason: "" }, events: [] }),
getRequestHistoryByID: async () => ({ requestSummary: { requestId: "1", queue: "repo/main", changeUri: "git://repo/main/sha", baseUri: "", requestState: "accepted", stateUpdatedAtMs: 100n, acceptedAtMs: 100n, outcomeReason: "", projectCount: 0, brokenProjectCount: 0 }, events: [] }),
} });
for (const href of ["/repo%2Fmain", "/repo%2Fmain/request/1", "/repo%2Fmain/request/1?view=history"]) {
const url = new URL(href, "https://submitqueue.test");
Expand Down
Loading