Skip to content

Commit 8ece598

Browse files
committed
PR_26171_007 Game Journey progress dashboard
1 parent b4d18b6 commit 8ece598

3 files changed

Lines changed: 156 additions & 4 deletions

File tree

tests/playwright/tools/GameJourneyTool.spec.mjs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ function statusLabel(statusId) {
102102
return status ? `${status.icon} ${status.label}` : statusId;
103103
}
104104

105+
function restoreEnvValue(key, value) {
106+
if (value === undefined) {
107+
delete process.env[key];
108+
return;
109+
}
110+
process.env[key] = value;
111+
}
112+
105113
function expectStaticToolOwnershipAreas(areas) {
106114
expect(areas.map((area) => area.sectionName)).toEqual([
107115
"Idea",
@@ -197,6 +205,83 @@ test("Game Journey exposes static tool ownership areas without automatic counts"
197205
}
198206
});
199207

208+
test("Game Journey progress dashboard summarizes completion metrics", async ({ page }) => {
209+
const server = await startRepoServer();
210+
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
211+
const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
212+
process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`;
213+
process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl;
214+
const failedRequests = [];
215+
const pageErrors = [];
216+
const consoleErrors = [];
217+
218+
page.on("pageerror", (error) => {
219+
pageErrors.push(error.message);
220+
});
221+
page.on("console", (message) => {
222+
if (message.type() === "error") {
223+
consoleErrors.push(message.text());
224+
}
225+
});
226+
page.on("response", (response) => {
227+
if (response.status() >= 400) {
228+
failedRequests.push(`${response.status()} ${response.url()}`);
229+
}
230+
});
231+
page.on("requestfailed", (request) => {
232+
failedRequests.push(`FAILED ${request.url()}`);
233+
});
234+
235+
try {
236+
await workspaceV2CoverageReporter.start(page);
237+
await fetch(`${server.baseUrl}/api/session/mode`, {
238+
body: JSON.stringify({ modeId: "local-db" }),
239+
headers: { "content-type": "application/json" },
240+
method: "POST",
241+
});
242+
await fetch(`${server.baseUrl}/api/session/user`, {
243+
body: JSON.stringify({ userKey: MOCK_DB_KEYS.users.user1 }),
244+
headers: { "content-type": "application/json" },
245+
method: "POST",
246+
});
247+
await page.goto(`${server.baseUrl}/toolbox/game-journey/index.html?game=demo-game`, { waitUntil: "networkidle" });
248+
249+
await expect(page.locator("[data-journey-active-game]")).toHaveText("Active game: Demo Game.");
250+
await expect(page.locator("[data-journey-progress-dashboard]")).toBeVisible();
251+
await expect(page.locator("details > summary", { hasText: /^Progress Dashboard$/ })).toHaveCount(1);
252+
await expect(page.locator("[data-journey-completion-metrics]")).toContainText("Completion model: 0 of 66 planned items complete (0%). Active buckets: 10; inactive buckets: 4.");
253+
await expect(page.locator("[data-journey-overall-progress] .mini-stat")).toHaveText([
254+
"0%Progress",
255+
"0Completed",
256+
"66Planned",
257+
"10Active Sections",
258+
]);
259+
await expect(page.locator("[data-journey-section-progress] table")).toHaveAttribute("aria-label", "Game Journey section progress");
260+
await expect(page.locator("[data-journey-completion-bucket]")).toHaveCount(14);
261+
await expect(page.locator("[data-journey-completion-bucket='001-idea'] td")).toHaveText([
262+
"Idea",
263+
"4",
264+
"0",
265+
"0%",
266+
"inactive",
267+
]);
268+
await expect(page.locator("[data-journey-most-complete-areas] li")).toHaveCount(3);
269+
await expect(page.locator("[data-journey-most-complete-areas] li").first()).toHaveText("Idea: 0% complete (0 of 4)");
270+
await expect(page.locator("[data-journey-least-complete-areas] li")).toHaveCount(3);
271+
await expect(page.locator("[data-journey-least-complete-areas] li").first()).toHaveText("Idea: 0% complete (0 of 4)");
272+
await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0);
273+
274+
expect(failedRequests).toEqual([]);
275+
expect(pageErrors).toEqual([]);
276+
expect(consoleErrors).toEqual([]);
277+
} finally {
278+
await workspaceV2CoverageReporter.stop(page);
279+
await server.close();
280+
restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl);
281+
restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl);
282+
}
283+
});
284+
200285
async function closeWithCoverage(page, failures) {
201286
await workspaceV2CoverageReporter.stop(page);
202287
await failures.server.close();

toolbox/game-journey/game-journey.js

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -719,16 +719,37 @@ function renderCompletionMetrics() {
719719
return;
720720
}
721721

722+
const dashboard = createElement("div", { className: "content-stack content-stack--compact" });
723+
dashboard.dataset.journeyProgressDashboard = "";
722724
const summary = createElement("p", {
723725
className: "status",
724726
text: `Completion model: ${completionMetricsSnapshot.completedCount} of ${completionMetricsSnapshot.plannedCount} planned items complete (${completionMetricsSnapshot.percentComplete}%). Active buckets: ${completionMetricsSnapshot.activeCount}; inactive buckets: ${completionMetricsSnapshot.inactiveCount}.`,
725727
});
728+
const overallHeading = createElement("h3", { text: "Overall Progress" });
729+
const overallGrid = createElement("div", { className: "grid cols-2" });
730+
overallGrid.dataset.journeyOverallProgress = "";
731+
[
732+
["Progress", `${completionMetricsSnapshot.percentComplete}%`],
733+
["Completed", String(completionMetricsSnapshot.completedCount)],
734+
["Planned", String(completionMetricsSnapshot.plannedCount)],
735+
["Active Sections", String(completionMetricsSnapshot.activeCount)],
736+
].forEach(([label, value]) => {
737+
const stat = createElement("article", { className: "mini-stat mini-stat--inline" });
738+
stat.append(
739+
createElement("strong", { text: value }),
740+
createElement("span", { text: label }),
741+
);
742+
overallGrid.append(stat);
743+
});
744+
745+
const sectionHeading = createElement("h3", { text: "Section Progress" });
726746
const wrapper = createElement("div", { className: "table-wrapper" });
747+
wrapper.dataset.journeySectionProgress = "";
727748
const table = createElement("table", { className: "data-table data-table--fixed" });
728-
table.setAttribute("aria-label", "Game Journey completion metrics");
749+
table.setAttribute("aria-label", "Game Journey section progress");
729750
const head = createElement("thead");
730751
const headRow = createElement("tr");
731-
["Bucket", "Planned", "Completed", "%", "Status"].forEach((heading) => {
752+
["Section", "Planned", "Completed", "%", "Status"].forEach((heading) => {
732753
const cell = createElement("th", { text: heading });
733754
cell.scope = "col";
734755
headRow.append(cell);
@@ -749,7 +770,53 @@ function renderCompletionMetrics() {
749770
});
750771
table.append(head, body);
751772
wrapper.append(table);
752-
completionMetrics.append(summary, wrapper);
773+
774+
const rankedRecords = records.filter((metric) => metric.plannedCount > 0);
775+
const mostComplete = [...rankedRecords]
776+
.sort((left, right) =>
777+
right.percentComplete - left.percentComplete ||
778+
right.completedCount - left.completedCount ||
779+
left.bucketKey.localeCompare(right.bucketKey),
780+
)
781+
.slice(0, 3);
782+
const leastComplete = [...rankedRecords]
783+
.sort((left, right) =>
784+
left.percentComplete - right.percentComplete ||
785+
left.completedCount - right.completedCount ||
786+
left.bucketKey.localeCompare(right.bucketKey),
787+
)
788+
.slice(0, 3);
789+
790+
const mostHeading = createElement("h3", { text: "Most Complete Areas" });
791+
const mostList = createElement("ol");
792+
mostList.dataset.journeyMostCompleteAreas = "";
793+
mostComplete.forEach((metric) => {
794+
mostList.append(createElement("li", {
795+
text: `${metric.bucketName}: ${metric.percentComplete}% complete (${metric.completedCount} of ${metric.plannedCount})`,
796+
}));
797+
});
798+
799+
const leastHeading = createElement("h3", { text: "Least Complete Areas" });
800+
const leastList = createElement("ol");
801+
leastList.dataset.journeyLeastCompleteAreas = "";
802+
leastComplete.forEach((metric) => {
803+
leastList.append(createElement("li", {
804+
text: `${metric.bucketName}: ${metric.percentComplete}% complete (${metric.completedCount} of ${metric.plannedCount})`,
805+
}));
806+
});
807+
808+
dashboard.append(
809+
summary,
810+
overallHeading,
811+
overallGrid,
812+
sectionHeading,
813+
wrapper,
814+
mostHeading,
815+
mostList,
816+
leastHeading,
817+
leastList,
818+
);
819+
completionMetrics.append(dashboard);
753820
}
754821

755822
function renderSearchStatus(query, notes) {

toolbox/game-journey/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ <h2>Journey Inspector</h2>
169169
</div>
170170
</details>
171171
<details class="vertical-accordion" open>
172-
<summary>Completion Metrics</summary>
172+
<summary>Progress Dashboard</summary>
173173
<div class="accordion-body">
174174
<div class="content-stack content-stack--compact" data-journey-completion-metrics></div>
175175
</div>

0 commit comments

Comments
 (0)