회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s
This commit is contained in:
parent
a479db7a5a
commit
a0311c5957
100 changed files with 4884 additions and 11210 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import path from "node:path";
|
||||
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||
import { expect, test, type Page, type Route, type TestInfo } from "@playwright/test";
|
||||
import type { DeliberatePracticeReadModel } from "../src/pages/session-review/deliberatePracticeApi";
|
||||
import { parsePracticeLaunchIntent } from "../src/lib/practiceLaunchIntent";
|
||||
import {
|
||||
|
|
@ -893,17 +893,14 @@ async function expectMobileActiveVisualIntegrity(page: Page) {
|
|||
expect(result.latestReplyVisible, diagnostic).toBe(true);
|
||||
}
|
||||
|
||||
async function capture(page: Page, projectName: string, stage: string) {
|
||||
async function capture(page: Page, testInfo: TestInfo, stage: string) {
|
||||
if (process.env.E2E_SKIP_EVIDENCE_CAPTURE === "1") return;
|
||||
const safeProject = projectName.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
||||
const evidenceDirectory = process.env.E2E_EVIDENCE_DIR
|
||||
? path.resolve(process.env.E2E_EVIDENCE_DIR)
|
||||
: path.resolve(process.cwd(), "../../docs/ops/evidence");
|
||||
const safeProject = testInfo.project.name.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
||||
const evidenceDirectory = process.env.E2E_EVIDENCE_DIR;
|
||||
await page.screenshot({
|
||||
path: path.resolve(
|
||||
evidenceDirectory,
|
||||
`self-directed-loop-${stage}-${safeProject}-2026-08-07.png`,
|
||||
),
|
||||
path: evidenceDirectory
|
||||
? path.resolve(evidenceDirectory, `self-directed-loop-${stage}-${safeProject}.png`)
|
||||
: testInfo.outputPath(`self-directed-loop-${stage}.png`),
|
||||
fullPage: true,
|
||||
animations: "disabled",
|
||||
});
|
||||
|
|
@ -1030,11 +1027,11 @@ test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fi
|
|||
);
|
||||
await expectMinimumHitTargets(page, ".lh-tabs [role='tab']", "320px 학습 홈 탭");
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await capture(page, testInfo.project.name, "home-320x568");
|
||||
await capture(page, testInfo, "home-320x568");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.evaluate(() => new Promise(requestAnimationFrame));
|
||||
}
|
||||
await capture(page, testInfo.project.name, "home");
|
||||
await capture(page, testInfo, "home");
|
||||
|
||||
await primaryAction.click();
|
||||
await expect(page).toHaveURL(/\/learn\/practice$/);
|
||||
|
|
@ -1095,11 +1092,11 @@ test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fi
|
|||
await expectMobileActiveVisualIntegrity(page);
|
||||
await expectCoachNudgeContrast(page);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await capture(page, testInfo.project.name, viewport.stage);
|
||||
await capture(page, testInfo, viewport.stage);
|
||||
}
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
} else {
|
||||
await capture(page, testInfo.project.name, "active-session");
|
||||
await capture(page, testInfo, "active-session");
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: "회기 종료" }).click();
|
||||
|
|
@ -1173,12 +1170,12 @@ test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fi
|
|||
"320px 처방 연습 시작 행동",
|
||||
);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await capture(page, testInfo.project.name, "review-prescription-320x568");
|
||||
await capture(page, testInfo, "review-prescription-320x568");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.evaluate(() => new Promise(requestAnimationFrame));
|
||||
}
|
||||
await practiceCard.scrollIntoViewIfNeeded();
|
||||
await capture(page, testInfo.project.name, "review-prescription");
|
||||
await capture(page, testInfo, "review-prescription");
|
||||
|
||||
const launch = practiceCard.getByRole("link", {
|
||||
name: "이 처방으로 연습 시작",
|
||||
|
|
@ -1222,7 +1219,7 @@ test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fi
|
|||
await page.evaluate(() => new Promise(requestAnimationFrame));
|
||||
await expectMinimumHitTargets(page, ".lh-actions .vg-btn", "320px 재연습 대상 행동");
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await capture(page, testInfo.project.name, "repractice-home-320x568");
|
||||
await capture(page, testInfo, "repractice-home-320x568");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.evaluate(() => new Promise(requestAnimationFrame));
|
||||
}
|
||||
|
|
@ -1264,11 +1261,11 @@ test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fi
|
|||
"320px 처방 재연습 시작 행동",
|
||||
);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await capture(page, testInfo.project.name, "repractice-prestart-320x568");
|
||||
await capture(page, testInfo, "repractice-prestart-320x568");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.evaluate(() => new Promise(requestAnimationFrame));
|
||||
}
|
||||
await capture(page, testInfo.project.name, "repractice-prestart");
|
||||
await capture(page, testInfo, "repractice-prestart");
|
||||
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await expect(page).toHaveURL(
|
||||
|
|
@ -1368,12 +1365,12 @@ test.describe("학습자 자기주도 전체 루프 — 실제 src UI / route fi
|
|||
await expect(observation).not.toContainText(RETRY_SESSION_ID.slice(0, 8));
|
||||
await expect(observation).not.toContainText("[NAME]");
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await capture(page, testInfo.project.name, "repractice-observation-320x568");
|
||||
await capture(page, testInfo, "repractice-observation-320x568");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.evaluate(() => new Promise(requestAnimationFrame));
|
||||
}
|
||||
await observation.scrollIntoViewIfNeeded();
|
||||
await capture(page, testInfo.project.name, "repractice-observation");
|
||||
await capture(page, testInfo, "repractice-observation");
|
||||
|
||||
await page.reload();
|
||||
observation = page.locator(".dp-runtime-observation");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import path from "node:path";
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { parseVoicePracticeContext } from "../src/lib/voicePracticeContext";
|
||||
import { expectNoHorizontalOverflow } from "./support";
|
||||
|
|
@ -424,6 +423,7 @@ async function routeMvpApi(page: Page, options: RouteMvpOptions = {}) {
|
|||
growthPoints: [{ title: "탐색 확장", body: "다음 턴에서 구체 상황을 더 묻습니다." }],
|
||||
nextLine: "그 말을 꺼내는 것도 쉽지 않았을 것 같아요.",
|
||||
clientFeedback: clientReply,
|
||||
learnerFeedbackEnabled: true,
|
||||
audioUrl: null,
|
||||
pdfExportUrl: null,
|
||||
degraded: false,
|
||||
|
|
@ -773,7 +773,6 @@ async function installVoiceLifecycleFixture(page: Page) {
|
|||
releaseEmptyFinal() {
|
||||
const socket = sockets[2];
|
||||
if (!socket || socket.readyState !== FixtureWebSocket.OPEN) return;
|
||||
socket.emitJson({ type: "state", state: "thinking" });
|
||||
socket.emitJson({ type: "transcript", text: "", final: true });
|
||||
socket.emitJson({ type: "state", state: "idle" });
|
||||
},
|
||||
|
|
@ -877,11 +876,12 @@ async function installVoiceLifecycleFixture(page: Page) {
|
|||
}
|
||||
if (payload.type !== "audio_end") return;
|
||||
window.setTimeout(() => {
|
||||
if (this.connectionNumber === 1) {
|
||||
this.emitJson({ type: "transcript", text: firstInterim, final: false });
|
||||
} else if (this.connectionNumber === 2) {
|
||||
this.emitJson({ type: "transcript", text: secondInterim, final: false });
|
||||
}
|
||||
if (this.connectionNumber > 2) return;
|
||||
this.emitJson({
|
||||
type: "transcript",
|
||||
text: this.connectionNumber === 1 ? firstInterim : secondInterim,
|
||||
final: false,
|
||||
});
|
||||
this.emitJson({
|
||||
type: "eot",
|
||||
ready: false,
|
||||
|
|
@ -1116,7 +1116,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
await expect(
|
||||
page.getByRole("heading", { name: "원본 회기 정보를 확인할 수 없습니다." }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "새 회기 시작" })).toBeDisabled();
|
||||
await expect(page.getByRole("button", { name: "완전히 새로 시작" })).toBeDisabled();
|
||||
await expectNoHorizontalOverflow(page);
|
||||
|
||||
await page.goto(`/learn/session/P1?${malformedQuery}`);
|
||||
|
|
@ -1153,11 +1153,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
await expect(page.getByRole("heading", { name: "첫 발화 전에 내 기준을 잠급니다" })).toBeVisible();
|
||||
await expect(page.getByLabel("학습자 발화 입력")).toBeDisabled();
|
||||
await page.screenshot({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
"../../docs/ops/evidence",
|
||||
`g1-alliance-pre-${testInfo.project.name}-2026-08-07.png`,
|
||||
),
|
||||
path: testInfo.outputPath("g1-alliance-pre.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
await selectAllianceScore(page, "목표", "3 보통이다");
|
||||
|
|
@ -1176,11 +1172,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
await expect(page.getByLabel("학습자 발화 입력")).toBeEnabled();
|
||||
await page.getByRole("button", { name: "30초 점검" }).click();
|
||||
await page.screenshot({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
"../../docs/ops/evidence",
|
||||
`g1-alliance-mid-${testInfo.project.name}-2026-08-07.png`,
|
||||
),
|
||||
path: testInfo.outputPath("g1-alliance-mid.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
await selectAllianceScore(page, "목표", "4 대체로 그렇다");
|
||||
|
|
@ -1210,7 +1202,8 @@ test.describe("P1 MVP core loop", () => {
|
|||
page.getByRole("button", { name: "회기 시작" }),
|
||||
diagnostics.join("\n") || (await page.locator("#root").innerText().catch(() => "")),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: /CBT/ }).click();
|
||||
await page.locator("details.sx-prestart__settings > summary").click();
|
||||
await page.getByRole("button", { name: /^CBT/ }).click();
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
|
||||
await expect(page.locator(".sx-page.sx-page--active")).toBeVisible();
|
||||
|
|
@ -1253,7 +1246,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.goto("/learn/session/P1");
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await page.getByRole("button", { name: "코칭" }).click();
|
||||
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
|
||||
await page.getByLabel("학습자 발화 입력").fill(learnerText);
|
||||
await page.getByRole("button", { name: "보내기" }).click();
|
||||
|
||||
|
|
@ -1292,7 +1285,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.goto("/learn/session/P1");
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await page.getByRole("button", { name: "코칭" }).click();
|
||||
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
|
||||
await page.getByLabel("학습자 발화 입력").fill(learnerText);
|
||||
await page.getByRole("button", { name: "보내기" }).click();
|
||||
|
||||
|
|
@ -1317,7 +1310,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.goto("/learn/session/P1");
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await page.getByRole("button", { name: "코칭" }).click();
|
||||
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
|
||||
await page.getByLabel("학습자 발화 입력").fill(learnerText);
|
||||
await page.getByRole("button", { name: "보내기" }).click();
|
||||
|
||||
|
|
@ -1341,7 +1334,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.goto("/learn/session/P1");
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await page.getByRole("button", { name: "코칭" }).click();
|
||||
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
|
||||
await page.getByLabel("학습자 발화 입력").fill(learnerText);
|
||||
await page.getByRole("button", { name: "보내기" }).click();
|
||||
|
||||
|
|
@ -1367,7 +1360,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.goto("/learn/session/P1");
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await page.getByRole("button", { name: "코칭" }).click();
|
||||
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
|
||||
await page.getByLabel("학습자 발화 입력").fill(learnerText);
|
||||
await page.getByRole("button", { name: "보내기" }).click();
|
||||
|
||||
|
|
@ -1505,6 +1498,14 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.getByRole("button", { name: "음성 다시 연결" }).click();
|
||||
await page.getByRole("button", { name: "발화 보내기" }).click();
|
||||
await expect.poll(async () =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
window.__voiceLifecycleFixture?.sent.filter((payload) =>
|
||||
payload.includes('"audio_end"'),
|
||||
).length ?? 0,
|
||||
),
|
||||
).toBe(3);
|
||||
await page.evaluate(() => window.__voiceLifecycleFixture?.releaseEmptyFinal());
|
||||
await expect(transcriptLog.locator(".sx-utt.is-learner")).toHaveCount(2);
|
||||
await expect(page.locator(".sx-mic-block__h")).toContainText("음성을 인식하지 못했습니다");
|
||||
|
|
@ -1519,7 +1520,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.goto("/learn/session/P1");
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await page.getByRole("button", { name: "코칭" }).click();
|
||||
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
|
||||
const micButton = page.getByRole("button", { name: "마이크 켜기" });
|
||||
await expect(micButton).toBeEnabled();
|
||||
await micButton.click();
|
||||
|
|
@ -1548,7 +1549,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.goto("/learn/session/P1");
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await page.getByRole("button", { name: "코칭" }).click();
|
||||
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
|
||||
await page.getByLabel("학습자 발화 입력").fill(learnerText);
|
||||
await page.getByRole("button", { name: "보내기" }).click();
|
||||
|
||||
|
|
@ -1583,7 +1584,7 @@ test.describe("P1 MVP core loop", () => {
|
|||
|
||||
await page.goto("/learn/session/P1");
|
||||
await page.getByRole("button", { name: "회기 시작" }).click();
|
||||
await page.getByRole("button", { name: "코칭" }).click();
|
||||
await page.getByRole("button", { name: /^코칭 모드, 남은 기회 \d+개$/ }).click();
|
||||
await page.getByLabel("학습자 발화 입력").fill("저 지금 자살하고 싶어요. 도와주세요.");
|
||||
await page.getByRole("button", { name: "보내기" }).click();
|
||||
|
||||
|
|
|
|||
|
|
@ -1117,10 +1117,10 @@ test.describe("voice cascade success path", () => {
|
|||
await expect(consentDialog).toBeVisible();
|
||||
if (process.env.CAPTURE_G7_CONSENT === "1") {
|
||||
const desktopViewport = page.viewportSize() ?? { width: 1440, height: 900 };
|
||||
await page.screenshot({ path: "test-results/g7-voice-consent-desktop.png" });
|
||||
await page.screenshot({ path: testInfo.outputPath("g7-voice-consent-desktop.png") });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expect(consentDialog).toBeVisible();
|
||||
await page.screenshot({ path: "test-results/g7-voice-consent-mobile.png" });
|
||||
await page.screenshot({ path: testInfo.outputPath("g7-voice-consent-mobile.png") });
|
||||
await page.setViewportSize(desktopViewport);
|
||||
}
|
||||
await expect.poll(async () => (await readVoiceUiProbe(page)).getUserMediaCalls).toBe(0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue