안전 이벤트 대시보드 보강
This commit is contained in:
parent
fe2796f05a
commit
51053af536
15 changed files with 1081 additions and 423 deletions
|
|
@ -559,7 +559,28 @@ test.describe("layout visual gate @single-run", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("professor student analysis stays contained across all widths", async ({ page }) => {
|
||||
test("professor student analysis overview stays contained across all widths", async ({ page }) => {
|
||||
const fixture = teacherAnalysisFixture();
|
||||
await page.route("**/api/teacher/dashboard", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(fixture.dashboard),
|
||||
}),
|
||||
);
|
||||
await signInAsTeacher(page);
|
||||
await page.goto("/teach/analysis");
|
||||
await gateScreen(page, "professor-analysis", async () => {
|
||||
await expect(page.locator('[data-learner-overview-table="true"]')).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.locator('[data-learner-overview-row="true"]')).toHaveCount(1);
|
||||
await expect(page.locator('[data-learner-analysis-panel="true"]')).toHaveCount(0);
|
||||
await expect(page.getByLabel("학습자 검색")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test("professor learner detail analysis stays contained across all widths", async ({ page }) => {
|
||||
const fixture = teacherAnalysisFixture();
|
||||
await page.route("**/api/teacher/dashboard", (route) =>
|
||||
route.fulfill({
|
||||
|
|
@ -576,12 +597,12 @@ test.describe("layout visual gate @single-run", () => {
|
|||
}),
|
||||
);
|
||||
await signInAsTeacher(page);
|
||||
await page.goto("/teach/analysis");
|
||||
await gateScreen(page, "professor-analysis", async () => {
|
||||
await expect(page.locator(".pf-analysis-shell")).toBeVisible({ timeout: 15_000 });
|
||||
await page.goto(`/teach/analysis?learner=${fixture.analysis.learner_id}`);
|
||||
await gateScreen(page, "professor-analysis-detail", async () => {
|
||||
await expect(page.locator('[data-learner-analysis-panel="true"]')).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await page.getByRole("tab", { name: /^전체 회기/ }).click();
|
||||
await expect(page.locator('[data-learner-session-row="true"]')).toHaveCount(5);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,17 @@ interface HealthResponse {
|
|||
engine?: boolean;
|
||||
}
|
||||
|
||||
interface TeacherSafetyAlert {
|
||||
session_id: string;
|
||||
resource_title: string;
|
||||
resource_number: string;
|
||||
}
|
||||
|
||||
interface TeacherDashboardResponse {
|
||||
source: string;
|
||||
safety_alerts: TeacherSafetyAlert[];
|
||||
}
|
||||
|
||||
interface ReviewTurn {
|
||||
speaker: string;
|
||||
text: string;
|
||||
|
|
@ -492,6 +503,72 @@ test.describe("session persistence", () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
test("persists crisis safety event into DB-backed teacher dashboard @single-run", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
|
||||
const healthResponse = await page.request.get("/api/health");
|
||||
await expectResponseOk(healthResponse);
|
||||
const health = (await healthResponse.json()) as HealthResponse;
|
||||
test.skip(!health.db, "crisis safety queue persistence requires DB");
|
||||
|
||||
await signInAsLearner(page);
|
||||
const persona = await fetchAvailablePersona(page, 1);
|
||||
const crisisText = "저 지금 자살하고 싶어요. 도와주세요.";
|
||||
|
||||
await page.goto(`/learn/session/${persona.code}`);
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await expect(page.locator(".sx-page.sx-page--active")).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page).toHaveURL(/\/learn\/session\/[0-9a-f-]+$/i);
|
||||
const sessionId = new URL(page.url()).pathname.split("/").at(-1);
|
||||
expect(sessionId).toMatch(/^[0-9a-f-]+$/i);
|
||||
|
||||
const streamResponsePromise = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url());
|
||||
return (
|
||||
response.request().method() === "POST" &&
|
||||
url.pathname.endsWith(`/api/sessions/${sessionId}/stream`)
|
||||
);
|
||||
});
|
||||
|
||||
const input = page.getByLabel("학습자 발화 입력");
|
||||
await input.fill(crisisText);
|
||||
await page.getByRole("button", { name: "보내기" }).click();
|
||||
|
||||
const streamResponse = await streamResponsePromise;
|
||||
await expectResponseOk(streamResponse);
|
||||
await expect(page.locator(".sx-crisis-resource")).toContainText("자살예방상담전화 109", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.getByRole("link", { name: "109" })).toBeVisible();
|
||||
await expect(input).toBeDisabled();
|
||||
|
||||
await signInAsTeacher(page);
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const dashboardResponse = await page.request.get("/api/teacher/dashboard");
|
||||
await expectResponseOk(dashboardResponse);
|
||||
const dashboard = (await dashboardResponse.json()) as TeacherDashboardResponse;
|
||||
expect(dashboard.source).toBe("database");
|
||||
return dashboard.safety_alerts.some(
|
||||
(alert) => alert.session_id === sessionId && alert.resource_number === "109",
|
||||
);
|
||||
},
|
||||
{
|
||||
timeout: 30_000,
|
||||
intervals: [500, 1_000, 2_000],
|
||||
message: "crisis safety event should reach the DB-backed teacher dashboard",
|
||||
},
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await page.goto("/teach");
|
||||
await expect(page.getByText("109 안전 확인 큐")).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.locator(".pf-alert").filter({ hasText: sessionId ?? "" })).toContainText("109");
|
||||
});
|
||||
|
||||
test("persists AI tutor coaching history through reload @single-run", async ({ page }) => {
|
||||
test.setTimeout(150_000);
|
||||
|
||||
|
|
|
|||
|
|
@ -640,16 +640,33 @@ test.describe("teacher console", () => {
|
|||
await page.getByRole("link", { name: "학생 분석" }).click();
|
||||
await expect(page).toHaveURL(/\/teach\/analysis$/);
|
||||
|
||||
const overviewTable = page.locator('[data-learner-overview-table="true"]');
|
||||
await expect(overviewTable).toBeVisible();
|
||||
await expect(page.locator('[data-learner-overview-row="true"]')).toHaveCount(2);
|
||||
await expect(page.locator('[data-learner-analysis-panel="true"]')).toHaveCount(0);
|
||||
|
||||
await page.getByLabel("학습자 검색").fill("사용자 B");
|
||||
const learnerBRow = page
|
||||
.locator('[data-learner-overview-row="true"]')
|
||||
.filter({ hasText: "사용자 B" });
|
||||
await expect(learnerBRow).toHaveCount(1);
|
||||
await expect(learnerBRow.getByText(learnerB)).toBeVisible();
|
||||
await learnerBRow.getByRole("button", { name: "사용자 B 상세 분석 열기" }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/teach/analysis\\?learner=${learnerB}$`));
|
||||
|
||||
const analysisPanel = page.locator('[data-learner-analysis-panel="true"]');
|
||||
await expect(analysisPanel).toBeVisible();
|
||||
await expect(analysisPanel.locator('[data-learner-session-row="true"]')).toHaveCount(7);
|
||||
await expect(analysisPanel.getByText(`${learnerA}-session-7`)).toBeVisible();
|
||||
await expect(page.getByText("전체 회기 7건")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "사용자 B 사용자별 분석" }).click();
|
||||
await expect(analysisPanel.locator('[data-learner-session-row="true"]')).toHaveCount(0);
|
||||
await analysisPanel.getByRole("tab", { name: /^전체 회기/ }).click();
|
||||
await expect(analysisPanel.locator('[data-learner-session-row="true"]')).toHaveCount(2);
|
||||
await expect(analysisPanel.getByText(`${learnerB}-session-2`)).toBeVisible();
|
||||
await expect(analysisPanel.getByText(`${learnerA}-session-7`)).toHaveCount(0);
|
||||
await expect(page.getByText("전체 회기 2건")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "전체 목록" }).click();
|
||||
await expect(page).toHaveURL(/\/teach\/analysis$/);
|
||||
await expect(page.locator('[data-learner-overview-table="true"]')).toBeVisible();
|
||||
await expect(page.locator('[data-learner-overview-row="true"]')).toHaveCount(1);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue