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
28 changes: 26 additions & 2 deletions apps/api/src/routes/v2/status-pages.http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,24 @@ describe("v2 status pages", () => {
const created = await harness.request("POST", "/v2/status_pages", token, {
name: "Acme Status",
slug: "acme-status",
brand_color: "#1A2B3C",
theme: { accent: "#1A2B3C" },
})
expect(created.status).toBe(200)
expect(created.body).toMatchObject({
object: "status_page",
name: "Acme Status",
slug: "acme-status",
brand_color: "#1a2b3c",
theme: {
preset: "default",
appearance: "auto",
accent: "#1a2b3c",
background: null,
text: null,
card: null,
border: null,
radius: "md",
font: "sans",
},
auto_incidents: "off",
revision: 1,
published_revision: 0,
Expand All @@ -152,8 +162,22 @@ describe("v2 status pages", () => {
const updated = await harness.request("PATCH", `/v2/status_pages/${id}`, token, {
description: "Live status of Acme.",
auto_incidents: "draft",
theme: { preset: "paper", appearance: "light", background: "#FFFDF7" },
})
expect(updated.status).toBe(200)
// Omitted theme fields keep their values.
expect(updated.body.theme).toMatchObject({
preset: "paper",
appearance: "light",
accent: "#1a2b3c",
background: "#fffdf7",
})

const unreadable = await harness.request("PATCH", `/v2/status_pages/${id}`, token, {
theme: { text: "#777777", background: "#ffffff" },
})
expect(unreadable.status).toBe(400)
expect(unreadable.body.error.message).toContain("4.48:1")
expect(updated.body).toMatchObject({
name: "Acme Status",
description: "Live status of Acme.",
Expand Down
15 changes: 7 additions & 8 deletions apps/api/src/routes/v2/status-pages.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type {
StatusPageIncidentKind,
StatusPageIncidentStatus,
} from "@maple/domain/primitives"
import { isActiveIncident } from "@maple/domain/status-page"
import { DEFAULT_STATUS_PAGE_THEME, isActiveIncident } from "@maple/domain/status-page"
import { Effect, Option } from "effect"
import { auditDiff } from "@/routes/v2/audit-changes"
import { recordHttpAudit } from "@maple/backend/services/audit/AuditLogService"
Expand All @@ -50,7 +50,7 @@ export const toV2StatusPage = (page: StatusPageRecord): V2StatusPage => ({
description: page.description,
website_url: page.websiteUrl,
logo_url: page.logoUrl,
brand_color: page.brandColor,
theme: page.theme,
show_powered_by: page.showPoweredBy,
auto_incidents: page.autoIncidents,
break_glass_token_suffix: page.breakGlassTokenSuffix,
Expand Down Expand Up @@ -109,7 +109,7 @@ const pageAuditDiff = auditDiff({
"description",
"website_url",
"logo_url",
"brand_color",
"theme",
"show_powered_by",
"auto_incidents",
],
Expand Down Expand Up @@ -191,9 +191,9 @@ export const HttpV2StatusPagesLive = HttpApiBuilder.group(MapleApiV2, "statusPag
? undefined
: { websiteUrl: payload.website_url }),
...(payload.logo_url === undefined ? undefined : { logoUrl: payload.logo_url }),
...(payload.brand_color === undefined
...(payload.theme === undefined
? undefined
: { brandColor: payload.brand_color }),
: { theme: { ...DEFAULT_STATUS_PAGE_THEME, ...payload.theme } }),
...(payload.show_powered_by === undefined
? undefined
: { showPoweredBy: payload.show_powered_by }),
Expand Down Expand Up @@ -224,9 +224,8 @@ export const HttpV2StatusPagesLive = HttpApiBuilder.group(MapleApiV2, "statusPag
? undefined
: { websiteUrl: payload.website_url }),
...(payload.logo_url === undefined ? undefined : { logoUrl: payload.logo_url }),
...(payload.brand_color === undefined
? undefined
: { brandColor: payload.brand_color }),
// Omitted theme fields keep the page's current values.
...(payload.theme === undefined ? undefined : { theme: payload.theme }),
...(payload.show_powered_by === undefined
? undefined
: { showPoweredBy: payload.show_powered_by }),
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@maple/infra": "workspace:*",
"@maple/query-engine": "workspace:*",
"@maple/query-model": "workspace:*",
"@maple/status-page": "workspace:*",
"@maple/ui": "workspace:*",
"@maple/web-data": "workspace:*",
"@maple/widgets": "workspace:*",
Expand Down
76 changes: 71 additions & 5 deletions apps/web/src/page/alerts/status-pages/editor/editor.scene.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { OrgId } from "@maple/domain"
import { StatusPagePublicId, type V2StatusPage } from "@maple/domain/http/v2"
import { Result, Schema } from "effect"
import { DEFAULT_STATUS_PAGE_THEME } from "@maple/domain/status-page"
import { Option, Result, Schema } from "effect"
import {
attr,
click,
expect as expectView,
find,
given,
inside,
Mount,
role,
scene,
selector,
tap,
text,
type,
withViewInputs,
Expand Down Expand Up @@ -92,6 +97,20 @@ describe("status page editor", () => {
expect(sent).toContain("FetchStatusPageMonitors")
})

test("fresh page and incident reads re-read the clock, so relative times never run ahead", () => {
const entered = init({ _tag: "AlertsStatusPage", pageId: PUBLIC_ID, search: {} }, context).model
const afterPage = pageUpdate(
entered,
Message.CompletedFetchPage({ key: entered.page.key ?? "", result: Result.succeed(statusPage()) }),
)
const afterIncidents = pageUpdate(
entered,
Message.CompletedFetchIncidents({ key: entered.incidents.key ?? "", result: Result.succeed([]) }),
)
expect(names(afterPage.commands)).toContain("ReadStatusPageClock")
expect(names(afterIncidents.commands)).toContain("ReadStatusPageClock")
})

test("an id that is not a page id shows not found without a read", () => {
const entered = init({ _tag: "AlertsStatusPage", pageId: "nope", search: {} }, context)
expect(names(entered.commands)).not.toContain("FetchStatusPage")
Expand Down Expand Up @@ -375,7 +394,7 @@ describe("settings tab", () => {
{ update: pageUpdate, view },
given(loaded({ tab: "settings" })),
expectView(text("Unsaved changes")).not.toExist(),
type(role("textbox", { name: "Brand color" }), "teal"),
type(role("textbox", { name: "Accent" }), "teal"),
expectView(text("Unsaved changes")).toExist(),
expectView(text("Use a hex color like #1a2b3c.")).toExist(),
expectView(role("button", { name: "Save" })).toBeDisabled(),
Expand All @@ -384,7 +403,8 @@ describe("settings tab", () => {

test("saving sends every field and settles once the page matches", () => {
const edited = [
Message.ChangedSetting({ field: "brandColor", value: "#1A2B3C" }),
Message.ChangedThemeColor({ field: "accent", value: "#1A2B3C" }),
Message.PickedThemePreset({ preset: "paper" }),
Message.ChangedSetting({ field: "autoIncidents", value: "publish" }),
Message.ToggledPoweredBy(),
].reduce((model, message) => pageUpdate(model, message).model, loaded({ tab: "settings" }))
Expand All @@ -396,7 +416,7 @@ describe("settings tab", () => {
payload: {
name: "Acme Status",
slug: "acme-status",
brand_color: "#1a2b3c",
theme: { ...DEFAULT_STATUS_PAGE_THEME, preset: "paper", accent: "#1a2b3c" },
show_powered_by: false,
auto_incidents: "publish",
website_url: null,
Expand All @@ -405,7 +425,7 @@ describe("settings tab", () => {
},
])
const saved = statusPage({
brand_color: "#1A2B3C",
theme: { ...DEFAULT_STATUS_PAGE_THEME, preset: "paper", accent: "#1a2b3c" },
show_powered_by: false,
auto_incidents: "publish",
})
Expand All @@ -416,6 +436,52 @@ describe("settings tab", () => {
expect(done.model.settingsEdits).toBeNull()
})

test("Auto disables the colour overrides but not the accent", () => {
scene(
{ update: pageUpdate, view },
given(loaded({ tab: "settings" })),
expectView(role("textbox", { name: "Background" })).toBeDisabled(),
expectView(role("textbox", { name: "Text" })).toBeDisabled(),
expectView(role("textbox", { name: "Accent" })).toBeEnabled(),
inside(role("group", { name: "Mode" }), click(role("button", { name: "Dark" }))),
expectView(role("textbox", { name: "Background" })).toBeEnabled(),
)
})

test("an unreadable text colour shows its contrast and blocks the save", () => {
scene(
{ update: pageUpdate, view },
given(loaded({ tab: "settings" })),
inside(role("group", { name: "Mode" }), click(role("button", { name: "Light" }))),
type(role("textbox", { name: "Text" }), "#777777"),
expectView(
text("Text on the background has a contrast of 4.48:1; it needs at least 4.5:1."),
).toExist(),
expectView(role("button", { name: "Save" })).toBeDisabled(),
)
})

test("the preview is a sandboxed page that follows the draft accent and preview mode", () => {
const iframeOf = find("iframe")
const srcdoc = (html: Parameters<typeof iframeOf>[0]): string =>
Option.getOrElse(Option.flatMap(iframeOf(html), attr("srcdoc")), () => "")
scene(
{ update: pageUpdate, view },
given(loaded({ tab: "settings" })),
expectView(selector("iframe")).toHaveAttr("sandbox", ""),
tap(({ html }) => {
expect(srcdoc(html)).toContain("Acme Status")
expect(srcdoc(html)).toContain("color-scheme:light;")
}),
// The preview mode is for looking only: it is not a page edit.
inside(role("group", { name: "Preview mode" }), click(role("button", { name: "Dark" }))),
tap(({ html }) => expect(srcdoc(html)).toContain("color-scheme:dark;")),
expectView(text("Unsaved changes")).not.toExist(),
type(role("textbox", { name: "Accent" }), "#ff0066"),
tap(({ html }) => expect(srcdoc(html)).toContain("--link:#ff0066")),
)
})

test("the first break-glass token is issued at once and shown once", () => {
const issuing = pageUpdate(loaded({ tab: "settings" }), Message.ClickedRotateToken())
expect(names(issuing.commands)).toEqual(["RotateStatusPageBreakGlassToken"])
Expand Down
37 changes: 33 additions & 4 deletions apps/web/src/page/alerts/status-pages/editor/form.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,53 @@
import { DEFAULT_STATUS_PAGE_THEME } from "@maple/domain/status-page"
import { describe, expect, it } from "vitest"
import { isSameSettings, type SettingsForm } from "./form"
import { hasSettingsIssues, isSameSettings, type SettingsForm, settingsPayload, themeFormOf } from "./form"

const saved: SettingsForm = {
name: "Acme!",
slug: "acme",
description: "",
websiteUrl: "https://acme.example",
logoUrl: "",
brandColor: "#aabbcc",
showPoweredBy: true,
autoIncidents: "off",
theme: themeFormOf({ ...DEFAULT_STATUS_PAGE_THEME, accent: "#aabbcc" }),
}

describe("isSameSettings", () => {
it("treats a draft the API stored trimmed and lower-cased as saved", () => {
const draft = { ...saved, name: "Acme! ", websiteUrl: " https://acme.example", brandColor: "#AABBCC" }
const draft = {
...saved,
name: "Acme! ",
websiteUrl: " https://acme.example",
theme: { ...saved.theme, accent: " #AABBCC" },
}
expect(isSameSettings(draft, saved)).toBe(true)
})

it("still sees a real change", () => {
expect(isSameSettings({ ...saved, brandColor: "#aabbcd" }, saved)).toBe(false)
expect(isSameSettings({ ...saved, theme: { ...saved.theme, accent: "#aabbcd" } }, saved)).toBe(false)
expect(isSameSettings({ ...saved, theme: { ...saved.theme, radius: "lg" } }, saved)).toBe(false)
})
})

describe("theme settings", () => {
it("sends the whole theme with blank colours as null", () => {
const form: SettingsForm = {
...saved,
theme: { ...saved.theme, appearance: "dark", background: "#0B0B0B " },
}
expect(settingsPayload(form).theme).toEqual({
...DEFAULT_STATUS_PAGE_THEME,
appearance: "dark",
accent: "#aabbcc",
background: "#0b0b0b",
})
})

it("blocks a bad hex and unreadable text, but not overrides Auto ignores", () => {
expect(hasSettingsIssues({ ...saved, theme: { ...saved.theme, card: "teal" } })).toBe(true)
const lowContrast = { ...saved.theme, text: "#777777", background: "#ffffff" }
expect(hasSettingsIssues({ ...saved, theme: { ...lowContrast, appearance: "light" } })).toBe(true)
expect(hasSettingsIssues({ ...saved, theme: lowContrast })).toBe(false)
})
})
Loading
Loading