회기 집계와 기록 복귀 및 감독 근거 링크 수정
Some checks failed
API contract / OpenAPI type drift (push) Failing after 4m46s
Some checks failed
API contract / OpenAPI type drift (push) Failing after 4m46s
This commit is contained in:
parent
aa951c6c4a
commit
1306c524c1
20 changed files with 4631 additions and 60 deletions
|
|
@ -99,6 +99,23 @@ async function routeReviewShell(page: Page, reviewSessionId = SESSION_ID) {
|
|||
);
|
||||
}
|
||||
|
||||
function routeLearnerReadModel(
|
||||
page: Page,
|
||||
expectedSessionIds: readonly string[],
|
||||
handler: (route: Route) => void | Promise<void>,
|
||||
) {
|
||||
return page.route(
|
||||
(url) =>
|
||||
url.pathname === "/api/calibration/learners/me" &&
|
||||
url.searchParams.has("session_id"),
|
||||
async (route) => {
|
||||
const sessionId = new URL(route.request().url()).searchParams.get("session_id");
|
||||
expect(expectedSessionIds).toContain(sessionId);
|
||||
await handler(route);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function initialReadModel(
|
||||
requestedView: "learner" | "supervisor" = "learner",
|
||||
): CalibrationTransferReadModelResponse {
|
||||
|
|
@ -516,7 +533,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => {
|
|||
await routeUnmockedApi(page);
|
||||
await routeAuth(page, "learner");
|
||||
await routeReviewShell(page);
|
||||
await page.route("**/api/calibration/learners/me", (route) =>
|
||||
await routeLearnerReadModel(page, [SESSION_ID], (route) =>
|
||||
fulfillReadModel(route, readModel),
|
||||
);
|
||||
await page.route("**/api/calibration/predictions/revisions", async (route) => {
|
||||
|
|
@ -687,7 +704,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => {
|
|||
await routeUnmockedApi(page);
|
||||
await routeAuth(page, "learner");
|
||||
await routeReviewShell(page);
|
||||
await page.route("**/api/calibration/learners/me", (route) =>
|
||||
await routeLearnerReadModel(page, [SESSION_ID], (route) =>
|
||||
fulfillReadModel(route, readModel),
|
||||
);
|
||||
|
||||
|
|
@ -805,7 +822,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => {
|
|||
await routeUnmockedApi(page);
|
||||
await routeAuth(page, "learner");
|
||||
await routeReviewShell(page, PRACTICE_SESSION_ID);
|
||||
await page.route("**/api/calibration/learners/me", async (route) => {
|
||||
await routeLearnerReadModel(page, [PRACTICE_SESSION_ID], async (route) => {
|
||||
readRequests += 1;
|
||||
await fulfillReadModel(route, readModel);
|
||||
});
|
||||
|
|
@ -935,7 +952,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => {
|
|||
await routeAuth(page, "learner");
|
||||
await routeReviewShell(page, PRACTICE_SESSION_ID);
|
||||
await routeReviewShell(page, SESSION_ID);
|
||||
await page.route("**/api/calibration/learners/me", (route) =>
|
||||
await routeLearnerReadModel(page, [SESSION_ID, PRACTICE_SESSION_ID], (route) =>
|
||||
fulfillReadModel(route, readModel),
|
||||
);
|
||||
await page.route("**/api/calibration/transfer-executions", async (route) => {
|
||||
|
|
@ -997,7 +1014,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => {
|
|||
await routeUnmockedApi(page);
|
||||
await routeAuth(page, "learner");
|
||||
await routeReviewShell(page);
|
||||
await page.route("**/api/calibration/learners/me", (route) =>
|
||||
await routeLearnerReadModel(page, [SESSION_ID], (route) =>
|
||||
fulfillReadModel(route, readModel),
|
||||
);
|
||||
|
||||
|
|
@ -1026,7 +1043,7 @@ test.describe("G5 calibration mirror and unseen transfer", () => {
|
|||
await routeUnmockedApi(page);
|
||||
await routeAuth(page, "learner");
|
||||
await routeReviewShell(page);
|
||||
await page.route("**/api/calibration/learners/me", async (route) => {
|
||||
await routeLearnerReadModel(page, [SESSION_ID], async (route) => {
|
||||
if (responseMode === "pending") {
|
||||
await firstResponse;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,8 +239,8 @@ test.describe("uc learner practice/history", () => {
|
|||
"직장 내 대인관계 갈등을 호소",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "새 회기 시작" }).click();
|
||||
await expect(page).toHaveURL(/\/learn\/session\/P2$/);
|
||||
await page.getByRole("button", { name: "완전히 새로 시작" }).click();
|
||||
await expect(page).toHaveURL(/\/learn\/session\/P2\?continuity=fresh$/);
|
||||
});
|
||||
|
||||
// usecase: 기록이 없는 신규 학습자에게 기초 난도부터 추천하되, 사용 불가
|
||||
|
|
@ -438,6 +438,96 @@ test.describe("uc learner practice/history", () => {
|
|||
await expect(page).toHaveURL(new RegExp(`/learn/session/${REV_ID}/review$`));
|
||||
});
|
||||
|
||||
test("검색과 리뷰 필터를 URL에 보존해 리뷰 왕복과 새로고침 뒤에도 복구한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installFixtures(page);
|
||||
await page.goto("/learn/history?source=dashboard");
|
||||
|
||||
const search = page.getByLabel("기록 검색");
|
||||
const chips = page.locator(".lh-history-filter");
|
||||
const historyLength = await page.evaluate(() => window.history.length);
|
||||
await search.pressSequentially("성하늘");
|
||||
await expect(search).toHaveValue("성하늘");
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("q"))
|
||||
.toBe("성하늘");
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.history.length))
|
||||
.toBe(historyLength);
|
||||
|
||||
await chips.getByRole("button", { name: "리뷰 필요" }).click();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("status"))
|
||||
.toBe("review");
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("source"))
|
||||
.toBe("dashboard");
|
||||
|
||||
const revCard = page.locator(".lh-session-card", {
|
||||
hasText: "AI 피드백 대기",
|
||||
});
|
||||
await revCard.getByRole("button", { name: "리뷰", exact: true }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/learn/session/${REV_ID}/review$`));
|
||||
|
||||
await page.goBack();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("q"))
|
||||
.toBe("성하늘");
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("status"))
|
||||
.toBe("review");
|
||||
await expect(search).toHaveValue("성하늘");
|
||||
await expect(chips.getByRole("button", { name: "리뷰 필요" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
await expect(page.locator(".lh-session-card")).toHaveCount(1);
|
||||
|
||||
await page.reload();
|
||||
await expect(search).toHaveValue("성하늘");
|
||||
await expect(chips.getByRole("button", { name: "리뷰 필요" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
|
||||
await search.fill("");
|
||||
await chips.getByRole("button", { name: "전체" }).click();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.has("q"))
|
||||
.toBe(false);
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.has("status"))
|
||||
.toBe(false);
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("source"))
|
||||
.toBe("dashboard");
|
||||
});
|
||||
|
||||
test("잘못된 상태 query는 전체 필터로 안전하게 처리한다", async ({ page }) => {
|
||||
await installFixtures(page);
|
||||
await page.goto("/learn/history?status=unknown&q=%EA%B9%80%EB%8F%84%EC%9C%A4&source=dashboard");
|
||||
|
||||
const chips = page.locator(".lh-history-filter");
|
||||
await expect(page.getByLabel("기록 검색")).toHaveValue("김도윤");
|
||||
await expect(chips.getByRole("button", { name: "전체" })).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
await expect(page.locator(".lh-session-card")).toHaveCount(1);
|
||||
|
||||
await chips.getByRole("button", { name: "리뷰 필요" }).click();
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("status"))
|
||||
.toBe("review");
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("q"))
|
||||
.toBe("김도윤");
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).searchParams.get("source"))
|
||||
.toBe("dashboard");
|
||||
});
|
||||
|
||||
// usecase: 종료된 기록 카드에서 다음 회기를 열어 같은 내담자의 맥락을 이어간다.
|
||||
test("종료 기록의 다음 회기 이어가기 버튼이 같은 내담자 새 회기로 이동한다", async ({
|
||||
page,
|
||||
|
|
@ -450,7 +540,7 @@ test.describe("uc learner practice/history", () => {
|
|||
await endCard
|
||||
.getByRole("button", { name: "다음 회기 이어가기", exact: true })
|
||||
.click();
|
||||
await expect(page).toHaveURL(/\/learn\/session\/P2$/);
|
||||
await expect(page).toHaveURL(/\/learn\/session\/P2\?continuity=continue$/);
|
||||
});
|
||||
|
||||
// usecase: 상태 필터 칩과 검색어를 함께 걸어 조건을 좁히고, 조건이 어긋나면
|
||||
|
|
|
|||
|
|
@ -760,9 +760,9 @@ test.describe("회기 대화 — 텍스트 턴과 SSE 스트림", () => {
|
|||
await resumeSession(page);
|
||||
|
||||
const transcript = page.locator(".sx-transcript__scroll");
|
||||
// 조사까지 자연스럽게 바뀐다: [NAME]은→익명 내담자는, [NAME]이→익명 내담자가.
|
||||
await expect(transcript).toContainText("익명 내담자는 제 얘기를 끝까지 듣지 않아요.");
|
||||
await expect(transcript).toContainText("익명 내담자가 자꾸 저를 몰아세워요.");
|
||||
// 조사까지 자연스럽게 바뀐다: [NAME]은→익명 인물은, [NAME]이→익명 인물이.
|
||||
await expect(transcript).toContainText("익명 인물은 제 얘기를 끝까지 듣지 않아요.");
|
||||
await expect(transcript).toContainText("익명 인물이 자꾸 저를 몰아세워요.");
|
||||
await expect(transcript).not.toContainText("[NAME]");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -562,6 +562,59 @@ test.describe("uc-teacher-console: 수퍼비전 attention 큐", () => {
|
|||
page.locator(".pf-depth-strip .is-current"),
|
||||
).toContainText("학습자 상세");
|
||||
});
|
||||
|
||||
test("legacy 감독 근거 링크는 엄격히 교사 경로로 정규화하고 canonical 경로는 유지한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
const legacySessionId = "62000000-0000-0000-0000-000000000801";
|
||||
const canonicalSessionId = "62000000-0000-0000-0000-000000000802";
|
||||
await routeUnmockedApi(page);
|
||||
await routeAuthedTeacher(page);
|
||||
await routeSupervision(page, [
|
||||
attentionItemFixture(1, "legacy-session", "growth_stagnation", [
|
||||
`/teacher/sessions/${legacySessionId}/`,
|
||||
]),
|
||||
attentionItemFixture(2, "canonical-session", "deterioration", [
|
||||
`/teach/session/${canonicalSessionId}/review`,
|
||||
]),
|
||||
attentionItemFixture(3, "legacy-dashboard", "safety_boundary", [
|
||||
"/teacher/dashboard",
|
||||
]),
|
||||
]);
|
||||
await routeConsole(page, {
|
||||
pending_reviews: [
|
||||
{
|
||||
...pendingReviewFixture(1, "62000000-0000-0000-0000-000000000111"),
|
||||
session_id: legacySessionId,
|
||||
},
|
||||
],
|
||||
});
|
||||
await routeTeacherSessionReview(page, legacySessionId);
|
||||
await routeSupervisorCalibration(page, emptyCalibrationReadModelFixture());
|
||||
|
||||
await page.goto("/teach/supervision");
|
||||
|
||||
await expect(page.locator(".g6-drilldown a")).toHaveAttribute(
|
||||
"href",
|
||||
`/teach/session/${legacySessionId}/review`,
|
||||
);
|
||||
await page.getByRole("button", { name: /canonical-session/ }).click();
|
||||
await expect(page.locator(".g6-drilldown a")).toHaveAttribute(
|
||||
"href",
|
||||
`/teach/session/${canonicalSessionId}/review`,
|
||||
);
|
||||
await page.getByRole("button", { name: /legacy-dashboard/ }).click();
|
||||
await expect(page.locator(".g6-drilldown a")).toHaveAttribute("href", "/teach");
|
||||
await page.getByRole("button", { name: /legacy-session/ }).click();
|
||||
await page.locator(".g6-drilldown a").click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/teach/session/${legacySessionId}/review$`),
|
||||
);
|
||||
await expect(
|
||||
page.getByText("교수자 검토 화면", { exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("uc-teacher-console: 학습자 상세 측정 근거", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue