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
30 changes: 24 additions & 6 deletions hub/client/application/hub/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,16 @@ export const buildSchema = z.object({
repositoryId: recordIdentifierSchema,
prototypeId: recordIdentifierSchema,
state: buildStateSchema,
commitSha: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i),
commitAuthorLogin: z.string().min(1),
commitSha: z
.string()
.regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i)
.nullable(),
commitAuthorProvider: z.string().min(1).nullable().optional(),
commitAuthorIssuer: z.string().min(1).nullable().optional(),
commitAuthorProviderUserId: z.string().min(1).nullable().optional(),
commitAuthorLogin: z.string().nullable(),
commitAuthorName: z.string().nullable().optional(),
commitAuthorAvatarUrl: httpUrl.nullable().optional(),
createdAt: timestamp,
startedAt: timestamp.nullable(),
finishedAt: timestamp.nullable(),
Expand All @@ -52,7 +60,9 @@ export const prototypeSchema = z.object({
name: z.string().min(1),
branchName: z.string().min(1),
description: z.string().nullable(),
authorLogin: z.string().min(1),
authorLogin: z.string().min(1).nullable(),
authorName: z.string().nullable().optional(),
authorProvider: z.string().min(1).nullable().optional(),
canEdit: z.boolean(),
latestBuild: z.object({ number: identifier, state: buildStateSchema }).nullable().optional(),
builds: z.array(buildSchema),
Expand Down Expand Up @@ -147,6 +157,14 @@ export const routeQuerySchema = z.object({
build: z.coerce.number().int().positive().optional(),
})

export const prototypeQuerySchema = routeQuerySchema.pick({ build: true }).extend({
view: z.enum(["gallery", "list", "minimized"]).optional(),
})
export const prototypeQuerySchema = z
.object({
build: z
.string()
.regex(/^[1-9]\d*$/)
.transform(Number)
.pipe(z.number().int())
.optional(),
view: z.enum(["gallery", "list", "minimized"]).optional(),
})
.strict()
7 changes: 5 additions & 2 deletions hub/client/application/hub/mocks/create-mock-store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,17 @@ export function createMockStore({ origin, seed = createFixtureSeed() }: Options)
throw new Error("The mock fixture contains duplicate records.")
}
for (const prototype of data.prototypes) {
if (!repositories.has(prototype.repositoryId) || !userLogins.has(prototype.authorLogin)) {
if (
!repositories.has(prototype.repositoryId) ||
(prototype.authorLogin !== null && !userLogins.has(prototype.authorLogin))
) {
throw new Error("A mock prototype refers to a missing repository or user.")
}
}
const builds = data.builds.map((build) => {
if (
!prototypeKeys.has(`${build.repositoryId}/${build.prototypeId}`) ||
!userLogins.has(build.commitAuthorLogin)
(build.commitAuthorLogin !== null && !userLogins.has(build.commitAuthorLogin))
) {
throw new Error("A mock build refers to a missing prototype or commit author.")
}
Expand Down
3 changes: 2 additions & 1 deletion hub/client/application/hub/read-prototype-query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ export function readPrototypeQuery(request: Request): PrototypeQuery {
})

if (!result.success) {
const invalidBuild = result.error.issues.some((issue) => issue.path[0] === "build")
throw new Response(
result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" "),
{ status: 400 },
{ status: invalidBuild ? 404 : 400 },
)
}
return result.data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("getPrototype()", () => {
const result = Promise.resolve().then(() => getPrototype(request, "2001", "1"))

await expect(result).rejects.toBeInstanceOf(Response)
await expect(result).rejects.toMatchObject({ status: 400 })
await expect(result).rejects.toMatchObject({ status: 404 })
},
)

Expand Down
89 changes: 89 additions & 0 deletions hub/client/application/hub/routes/journey.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,17 @@ describe("Hub repository journey", () => {
expect(router.state.location.pathname).toBe(`/repositories/${repositoryId}`)
await router.settle(() => fireEvent.click(welcome))
expect(await screen.findByTitle(`Welcome, Build ${latestBuild} preview`)).toBeInTheDocument()
expect(screen.getByLabelText("By @octocat").closest("a")).toHaveAttribute(
"href",
"https://github.com/octocat",
)
expect(
screen.getByRole<HTMLInputElement>("textbox", { name: "Preview URL" }).value,
).not.toMatch(/mock|synthetic|simulat(?:ed|ion)/i)
fireEvent.click(screen.getByRole("button", { name: "More actions" }))
expect(
screen.getByRole("menuitem", { name: "Share links" }).querySelector("a"),
).toHaveAttribute("href", `/repositories/${repositoryId}/prototypes/${prototypeId}/share-links`)
const history = screen.getByRole("menuitem", { name: "History" }).querySelector("a")
if (!history) throw new Error("The History action has no link.")
await router.settle(() => fireEvent.click(history))
Expand All @@ -134,13 +141,95 @@ describe("Hub repository journey", () => {

expect(await screen.findByRole("heading", { name: "Build 2", level: 2 })).toBeInTheDocument()
expect(screen.getByText("Failed")).toBeInTheDocument()
expect(screen.getByText("By @mona").closest("a")).toHaveAttribute(
"href",
"https://github.com/mona",
)
if (!failedBuild.failureSummary) throw new Error("The failed Build needs an explanation.")
expect(screen.getByText(failedBuild.failureSummary)).toBeInTheDocument()
expect(router.state.location.pathname).toBe(
`/repositories/${repositoryId}/prototypes/${prototypeId}/builds/2`,
)
}, 15_000)

it("shows commit authors in Build history and details", async () => {
const store = createMockStore({ origin: window.location.origin })
const history = store.getBuildHistory("2001", "1")
const authors = [
{
provider: "entra",
login: "sam@example.test",
name: "Sam Rivera",
},
{
provider: "entra",
login: "lee@example.test",
name: null,
},
{
provider: "github",
login: "mona",
name: "Mona Lisa",
},
{ provider: null, login: null, name: null },
{ provider: null, login: "legacy-author", name: null },
]
const builds = authors.map((author, index) => ({
...history.builds[index]!,
commitAuthorProvider: author.provider,
commitAuthorLogin: author.login,
commitAuthorName: author.name,
}))
const server = setupServer(...createMockHandlers(store))
server.use(
http.get(`${window.location.origin}/api/repositories/2001/prototypes/1/builds`, () =>
HttpResponse.json({ ...history, builds }),
),
http.get(
`${window.location.origin}/api/repositories/2001/prototypes/1/builds/:number`,
({ params }) => {
const build = builds.find(({ number }) => String(number) === params.number)
if (!build) return new HttpResponse(null, { status: 404 })
return HttpResponse.json({ ...store.getBuild("2001", "1", build.number), build })
},
),
)
const router = await renderHubFlow(server, "/repositories/2001/prototypes/1/builds")

expect(screen.getByText("By Sam Rivera").closest("a")).toBeNull()
expect(screen.getByText("By lee@example.test").closest("a")).toBeNull()
expect(screen.getByText("By @mona").closest("a")).toHaveAttribute(
"href",
"https://github.com/mona",
)
expect(screen.getByText("Unknown author").closest("a")).toBeNull()
expect(screen.getByText("By legacy-author").closest("a")).toBeNull()

await router.settle(() =>
router.navigate(`/repositories/2001/prototypes/1/builds/${builds[0]!.number}`),
)
expect(screen.getByText("By Sam Rivera").closest("a")).toBeNull()
await router.settle(() =>
router.navigate(`/repositories/2001/prototypes/1/builds/${builds[1]!.number}`),
)
expect(screen.getByText("By lee@example.test").closest("a")).toBeNull()
await router.settle(() =>
router.navigate(`/repositories/2001/prototypes/1/builds/${builds[2]!.number}`),
)
expect(screen.getByText("By @mona").closest("a")).toHaveAttribute(
"href",
"https://github.com/mona",
)
await router.settle(() =>
router.navigate(`/repositories/2001/prototypes/1/builds/${builds[3]!.number}`),
)
expect(screen.getByText("Unknown author").closest("a")).toBeNull()
await router.settle(() =>
router.navigate(`/repositories/2001/prototypes/1/builds/${builds[4]!.number}`),
)
expect(screen.getByText("By legacy-author").closest("a")).toBeNull()
}, 15_000)

it("retains a failed edit draft and publishes a successful retry consistently", async () => {
const store = createMockStore({ origin: window.location.origin })
const server = setupServer(...createMockHandlers(store))
Expand Down
Loading
Loading