address review comments

This commit is contained in:
ryana 2026-01-20 02:42:54 +08:00
parent 4bdea1ed61
commit f9061ebb93
7 changed files with 234 additions and 101 deletions

View File

@ -16,7 +16,6 @@ class Incident extends BeanModel {
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {object} Object ready to parse * @returns {object} Object ready to parse
*/ */
toPublicJSON() { toPublicJSON() {
@ -29,16 +28,6 @@ class Incident extends BeanModel {
active: !!this.active, active: !!this.active,
createdDate: this.createdDate, createdDate: this.createdDate,
lastUpdatedDate: this.lastUpdatedDate, lastUpdatedDate: this.lastUpdatedDate,
};
}
/**
* Return full object for admin use
* @returns {object} Object ready to parse
*/
toJSON() {
return {
...this.toPublicJSON(),
status_page_id: this.status_page_id, status_page_id: this.status_page_id,
}; };
} }

View File

@ -502,28 +502,50 @@ class StatusPage extends BeanModel {
} }
/** /**
* Get paginated incident history for a status page * Get paginated incident history for a status page using cursor-based pagination
* @param {number} statusPageId ID of the status page * @param {number} statusPageId ID of the status page
* @param {number} page Page number (1-based) * @param {string|null} cursor ISO date string cursor (created_date of last item from previous page)
* @param {boolean} isPublic Whether to return public or admin data * @param {boolean} isPublic Whether to return public or admin data
* @returns {Promise<object>} Paginated incident data * @returns {Promise<object>} Paginated incident data with cursor
*/ */
static async getIncidentHistory(statusPageId, page, isPublic = true) { static async getIncidentHistory(statusPageId, cursor = null, isPublic = true) {
const offset = (page - 1) * INCIDENT_PAGE_SIZE; let incidents;
const incidents = await R.find("incident", " status_page_id = ? ORDER BY created_date DESC LIMIT ? OFFSET ? ", [ if (cursor) {
statusPageId, incidents = await R.find(
INCIDENT_PAGE_SIZE, "incident",
offset, " status_page_id = ? AND created_date < ? ORDER BY created_date DESC LIMIT ? ",
]); [statusPageId, cursor, INCIDENT_PAGE_SIZE]
);
} else {
incidents = await R.find("incident", " status_page_id = ? ORDER BY created_date DESC LIMIT ? ", [
statusPageId,
INCIDENT_PAGE_SIZE,
]);
}
const total = await R.count("incident", " status_page_id = ? ", [statusPageId]); const total = await R.count("incident", " status_page_id = ? ", [statusPageId]);
const lastIncident = incidents[incidents.length - 1];
let nextCursor = null;
let hasMore = false;
if (lastIncident) {
const moreCount = await R.count("incident", " status_page_id = ? AND created_date < ? ", [
statusPageId,
lastIncident.createdDate,
]);
hasMore = moreCount > 0;
if (hasMore) {
nextCursor = lastIncident.createdDate;
}
}
return { return {
incidents: incidents.map((i) => (isPublic ? i.toPublicJSON() : i.toJSON())), incidents: incidents.map((i) => i.toPublicJSON()),
total, total,
page, nextCursor,
totalPages: Math.ceil(total / INCIDENT_PAGE_SIZE), hasMore,
}; };
} }

View File

@ -155,8 +155,8 @@ router.get("/api/status-page/:slug/incident-history", cache("5 minutes"), async
return; return;
} }
const page = parseInt(request.query.page) || 1; const cursor = request.query.cursor || null;
const result = await StatusPage.getIncidentHistory(statusPageID, page, true); const result = await StatusPage.getIncidentHistory(statusPageID, cursor, true);
response.json({ response.json({
ok: true, ok: true,
...result, ...result,

View File

@ -99,36 +99,15 @@ module.exports.statusPageSocketHandler = (socket) => {
} }
}); });
socket.on("getIncidentHistory", async (slug, page, callback) => { socket.on("getIncidentHistory", async (slug, cursor, callback) => {
try {
checkLogin(socket);
let statusPageID = await StatusPage.slugToID(slug);
if (!statusPageID) {
throw new Error("slug is not found");
}
const result = await StatusPage.getIncidentHistory(statusPageID, page, false);
callback({
ok: true,
...result,
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("getPublicIncidentHistory", async (slug, page, callback) => {
try { try {
let statusPageID = await StatusPage.slugToID(slug); let statusPageID = await StatusPage.slugToID(slug);
if (!statusPageID) { if (!statusPageID) {
throw new Error("slug is not found"); throw new Error("slug is not found");
} }
const result = await StatusPage.getIncidentHistory(statusPageID, page, true); const isPublic = !socket.userID;
const result = await StatusPage.getIncidentHistory(statusPageID, cursor, isPublic);
callback({ callback({
ok: true, ok: true,
...result, ...result,
@ -193,7 +172,7 @@ module.exports.statusPageSocketHandler = (socket) => {
ok: true, ok: true,
msg: "Saved.", msg: "Saved.",
msgi18n: true, msgi18n: true,
incident: bean.toJSON(), incident: bean.toPublicJSON(),
}); });
} catch (error) { } catch (error) {
callback({ callback({
@ -274,7 +253,7 @@ module.exports.statusPageSocketHandler = (socket) => {
ok: true, ok: true,
msg: "Resolved", msg: "Resolved",
msgi18n: true, msgi18n: true,
incident: bean.toJSON(), incident: bean.toPublicJSON(),
}); });
} catch (error) { } catch (error) {
callback({ callback({

View File

@ -496,20 +496,12 @@
</div> </div>
<!-- Past Incidents --> <!-- Past Incidents -->
<div class="past-incidents-section mb-4"> <div v-if="pastIncidentCount > 0" class="past-incidents-section mb-4">
<h2 class="past-incidents-title mb-3">{{ $t("Past Incidents") }}</h2> <h2 class="past-incidents-title mb-3">
{{ $t("Past Incidents") }}
</h2>
<div v-if="incidentHistoryLoading && incidentHistory.length === 0" class="text-center py-4"> <div class="past-incidents-content">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{{ $t("Loading...") }}</span>
</div>
</div>
<div v-else-if="incidentHistory.length === 0" class="text-center py-4 text-muted">
{{ $t("No incidents recorded") }}
</div>
<template v-else>
<div <div
v-for="(dateGroup, dateKey) in groupedIncidentHistory" v-for="(dateGroup, dateKey) in groupedIncidentHistory"
:key="dateKey" :key="dateKey"
@ -528,10 +520,7 @@
</div> </div>
</div> </div>
<div <div v-if="incidentHistoryHasMore" class="load-more-controls d-flex justify-content-center mt-3">
v-if="incidentHistoryPage < incidentHistoryTotalPages"
class="load-more-controls d-flex justify-content-center mt-3"
>
<button <button
class="btn btn-outline-secondary btn-sm" class="btn btn-outline-secondary btn-sm"
:disabled="incidentHistoryLoading" :disabled="incidentHistoryLoading"
@ -545,7 +534,7 @@
{{ $t("Load More") }} {{ $t("Load More") }}
</button> </button>
</div> </div>
</template> </div>
</div> </div>
<!-- Incident Manage Modal --> <!-- Incident Manage Modal -->
@ -715,8 +704,8 @@ export default {
loading: true, loading: true,
incidentHistory: [], incidentHistory: [],
incidentHistoryLoading: false, incidentHistoryLoading: false,
incidentHistoryPage: 1, incidentHistoryNextCursor: null,
incidentHistoryTotalPages: 1, incidentHistoryHasMore: false,
}; };
}, },
computed: { computed: {
@ -878,12 +867,22 @@ export default {
}, },
/** /**
* Group incidents by date for display * Count of past incidents (non-active or unpinned)
* @returns {number} Number of past incidents
*/
pastIncidentCount() {
return this.incidentHistory.filter((i) => !(i.active && i.pin)).length;
},
/**
* Group past incidents (non-active or unpinned) by date for display
* Active+pinned incidents are shown separately at the top, not in this section
* @returns {object} Incidents grouped by date string * @returns {object} Incidents grouped by date string
*/ */
groupedIncidentHistory() { groupedIncidentHistory() {
const groups = {}; const groups = {};
for (const incident of this.incidentHistory) { const pastIncidents = this.incidentHistory.filter((i) => !(i.active && i.pin));
for (const incident of pastIncidents) {
const dateKey = this.formatDateKey(incident.createdDate); const dateKey = this.formatDateKey(incident.createdDate);
if (!groups[dateKey]) { if (!groups[dateKey]) {
groups[dateKey] = []; groups[dateKey] = [];
@ -998,7 +997,6 @@ export default {
this.$root.publicGroupList = res.data.publicGroupList; this.$root.publicGroupList = res.data.publicGroupList;
this.loading = false; this.loading = false;
this.loadIncidentHistory();
feedInterval = setInterval( feedInterval = setInterval(
() => { () => {
@ -1031,6 +1029,7 @@ export default {
}); });
this.updateHeartbeatList(); this.updateHeartbeatList();
this.loadIncidentHistory();
// Go to edit page if ?edit present // Go to edit page if ?edit present
// null means ?edit present, but no value // null means ?edit present, but no value
@ -1393,20 +1392,20 @@ export default {
* @returns {void} * @returns {void}
*/ */
loadIncidentHistory() { loadIncidentHistory() {
this.loadIncidentHistoryPage(1); this.loadIncidentHistoryWithCursor(null);
}, },
/** /**
* Load a specific page of incident history * Load incident history using cursor-based pagination
* @param {number} page - Page number to load * @param {string|null} cursor - Cursor for pagination (created_date of last item)
* @param {boolean} append - Whether to append to existing list * @param {boolean} append - Whether to append to existing list
* @returns {void} * @returns {void}
*/ */
loadIncidentHistoryPage(page, append = false) { loadIncidentHistoryWithCursor(cursor, append = false) {
this.incidentHistoryLoading = true; this.incidentHistoryLoading = true;
if (this.enableEditMode) { if (this.enableEditMode) {
this.$root.getSocket().emit("getIncidentHistory", this.slug, page, (res) => { this.$root.getSocket().emit("getIncidentHistory", this.slug, cursor, (res) => {
this.incidentHistoryLoading = false; this.incidentHistoryLoading = false;
if (res.ok) { if (res.ok) {
if (append) { if (append) {
@ -1414,16 +1413,19 @@ export default {
} else { } else {
this.incidentHistory = res.incidents; this.incidentHistory = res.incidents;
} }
this.incidentHistoryPage = res.page; this.incidentHistoryNextCursor = res.nextCursor;
this.incidentHistoryTotalPages = res.totalPages; this.incidentHistoryHasMore = res.hasMore;
} else { } else {
console.error("Failed to load incident history:", res.msg); console.error("Failed to load incident history:", res.msg);
this.$root.toastError(res.msg); this.$root.toastError(res.msg);
} }
}); });
} else { } else {
const url = cursor
? `/api/status-page/${this.slug}/incident-history?cursor=${encodeURIComponent(cursor)}`
: `/api/status-page/${this.slug}/incident-history`;
axios axios
.get(`/api/status-page/${this.slug}/incident-history?page=${page}`) .get(url)
.then((res) => { .then((res) => {
this.incidentHistoryLoading = false; this.incidentHistoryLoading = false;
if (res.data.ok) { if (res.data.ok) {
@ -1432,8 +1434,8 @@ export default {
} else { } else {
this.incidentHistory = res.data.incidents; this.incidentHistory = res.data.incidents;
} }
this.incidentHistoryPage = res.data.page; this.incidentHistoryNextCursor = res.data.nextCursor;
this.incidentHistoryTotalPages = res.data.totalPages; this.incidentHistoryHasMore = res.data.hasMore;
} }
}) })
.catch((error) => { .catch((error) => {
@ -1444,12 +1446,12 @@ export default {
}, },
/** /**
* Load more incident history (next page, appended) * Load more incident history using cursor-based pagination
* @returns {void} * @returns {void}
*/ */
loadMoreIncidentHistory() { loadMoreIncidentHistory() {
if (this.incidentHistoryPage < this.incidentHistoryTotalPages) { if (this.incidentHistoryHasMore && this.incidentHistoryNextCursor) {
this.loadIncidentHistoryPage(this.incidentHistoryPage + 1, true); this.loadIncidentHistoryWithCursor(this.incidentHistoryNextCursor, true);
} }
}, },
@ -1601,12 +1603,14 @@ footer {
/* Reset button placed at top-left of the logo */ /* Reset button placed at top-left of the logo */
.reset-top-left { .reset-top-left {
position: absolute; transition:
top: 0; transform $easing-in 0.18s,
left: -15px; box-shadow $easing-in 0.18s,
z-index: 2; background-color $easing-in 0.18s;
width: 20px; font-size: 18px;
height: 20px; width: 18px;
height: 18px;
padding: 0;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -1615,11 +1619,6 @@ footer {
border: none; border: none;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
cursor: pointer; cursor: pointer;
padding: 0;
transition:
transform $easing-in 0.18s,
box-shadow $easing-in 0.18s,
background-color $easing-in 0.18s;
transform-origin: center; transform-origin: center;
&:hover { &:hover {
@ -1762,6 +1761,12 @@ footer {
font-weight: normal; font-weight: normal;
} }
.past-incidents-section {
.past-incidents-content {
padding: 0;
}
}
.incident-date-group { .incident-date-group {
.incident-date-header { .incident-date-header {
font-size: 1rem; font-size: 1rem;

View File

@ -10,8 +10,8 @@
*/ */
var _a; var _a;
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.RESPONSE_BODY_LENGTH_MAX = exports.RESPONSE_BODY_LENGTH_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0; exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.RESPONSE_BODY_LENGTH_MAX = exports.RESPONSE_BODY_LENGTH_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.INCIDENT_PAGE_SIZE = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = void 0; exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = void 0;
const dayjs_1 = require("dayjs"); const dayjs_1 = require("dayjs");
const jsonata = require("jsonata"); const jsonata = require("jsonata");
exports.isDev = process.env.NODE_ENV === "development"; exports.isDev = process.env.NODE_ENV === "development";

View File

@ -0,0 +1,138 @@
import { expect, test } from "@playwright/test";
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
test.describe("Incident History", () => {
test.beforeEach(async ({ page }) => {
await restoreSqliteSnapshot(page);
});
test("past incidents section is hidden when no incidents exist", async ({ page }, testInfo) => {
test.setTimeout(60000);
await page.goto("./add");
await login(page);
await page.goto("./add-status-page");
await page.getByTestId("name-input").fill("Empty Test");
await page.getByTestId("slug-input").fill("empty-test");
await page.getByTestId("submit-button").click();
await page.waitForURL("/status/empty-test?edit");
await page.getByTestId("save-button").click();
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
const pastIncidentsSection = page.locator(".past-incidents-section");
await expect(pastIncidentsSection).toHaveCount(0);
await screenshot(testInfo, page);
});
test("active pinned incidents are shown at top and not in past incidents", async ({ page }, testInfo) => {
test.setTimeout(60000);
await page.goto("./add");
await login(page);
await page.goto("./add-status-page");
await page.getByTestId("name-input").fill("Dedup Test");
await page.getByTestId("slug-input").fill("dedup-test");
await page.getByTestId("submit-button").click();
await page.waitForURL("/status/dedup-test?edit");
await page.getByTestId("create-incident-button").click();
await page.getByTestId("incident-title").fill("Active Incident");
await page.getByTestId("incident-content-editable").fill("This is an active incident");
await page.getByTestId("post-incident-button").click();
await page.getByTestId("save-button").click();
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
const activeIncident = page.getByTestId("incident").filter({ hasText: "Active Incident" });
await expect(activeIncident).toBeVisible();
const pastIncidentsSection = page.locator(".past-incidents-section");
await expect(pastIncidentsSection).toHaveCount(0);
await screenshot(testInfo, page);
});
test("resolved incidents appear in past incidents section", async ({ page }, testInfo) => {
test.setTimeout(90000);
await page.goto("./add");
await login(page);
await page.goto("./add-status-page");
await page.getByTestId("name-input").fill("Resolve Test");
await page.getByTestId("slug-input").fill("resolve-test");
await page.getByTestId("submit-button").click();
await page.waitForURL("/status/resolve-test?edit");
await page.getByTestId("create-incident-button").click();
await page.getByTestId("incident-title").fill("Resolved Incident");
await page.getByTestId("incident-content-editable").fill("This incident will be resolved");
await page.getByTestId("post-incident-button").click();
await page.waitForTimeout(500);
const resolveButton = page.locator("button", { hasText: "Resolve" }).first();
await expect(resolveButton).toBeVisible();
await resolveButton.click();
await page.waitForTimeout(1000);
const activeIncident = page.getByTestId("incident").filter({ hasText: "Resolved Incident" });
await expect(activeIncident).toHaveCount(0);
const pastIncidentsSection = page.locator(".past-incidents-section");
await expect(pastIncidentsSection).toBeVisible();
const resolvedIncidentInHistory = pastIncidentsSection.locator("text=Resolved Incident");
await expect(resolvedIncidentInHistory).toBeVisible();
await screenshot(testInfo, page);
});
test("incident history pagination loads more incidents", async ({ page }, testInfo) => {
test.setTimeout(120000);
await page.goto("./add");
await login(page);
await page.goto("./add-status-page");
await page.getByTestId("name-input").fill("Pagination Test");
await page.getByTestId("slug-input").fill("pagination-test");
await page.getByTestId("submit-button").click();
await page.waitForURL("/status/pagination-test?edit");
for (let i = 1; i <= 12; i++) {
await page.getByTestId("create-incident-button").click();
await page.getByTestId("incident-title").fill("Incident " + i);
await page.getByTestId("incident-content-editable").fill("Content for incident " + i);
await page.getByTestId("post-incident-button").click();
await page.waitForTimeout(300);
const resolveButton = page.locator("button", { hasText: "Resolve" }).first();
if (await resolveButton.isVisible()) {
await resolveButton.click();
await page.waitForTimeout(300);
}
}
await page.getByTestId("save-button").click();
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
await page.waitForTimeout(1000);
const pastIncidentsSection = page.locator(".past-incidents-section");
await expect(pastIncidentsSection).toBeVisible();
const loadMoreButton = page.locator("button", { hasText: "Load More" });
if (await loadMoreButton.isVisible()) {
await loadMoreButton.click();
await page.waitForTimeout(1000);
await screenshot(testInfo, page);
}
});
});