- Update E2E tests: announcements, auth, auth-business-flow, full-route-regression, grades, navigation, smoke-auth, teacher-web-test - Update integration tests: api-ai-chat, api-onboarding-complete, api-onboarding-status, proxy-guard, integration setup - Update visual regression tests: admin-dashboard, homepage, student-dashboard, teacher-dashboard, visual config, helpers - Update webapp tests: admin, parent, student full tests and debug scripts - Add new webapp tests: announcements_messages, settings_profile, debug scripts - Add webtest directory with test plans, screenshots, and diagnostic scripts
249 lines
22 KiB
JSON
249 lines
22 KiB
JSON
{
|
||
"test_date": "2026-06-22 19:24:47",
|
||
"module": "选修课 (Elective)",
|
||
"version": "0.1.0",
|
||
"base_url": "http://localhost:3000",
|
||
"summary": {
|
||
"total": 22,
|
||
"passed": 21,
|
||
"failed": 0,
|
||
"warnings": 1
|
||
},
|
||
"roles": {
|
||
"admin": {
|
||
"role": "admin",
|
||
"login_success": true,
|
||
"pages": [
|
||
{
|
||
"url": "http://localhost:3000/admin/elective",
|
||
"route": "/admin/elective",
|
||
"category": "选修课程列表",
|
||
"status": "passed",
|
||
"http_status": 200,
|
||
"final_url": "http://localhost:3000/admin/elective",
|
||
"checks": [
|
||
{
|
||
"name": "标题关键词匹配 (选修课程)",
|
||
"passed": true,
|
||
"detail": "实际匹配: True"
|
||
}
|
||
],
|
||
"errors": [],
|
||
"warnings": [],
|
||
"console_errors": [],
|
||
"screenshot": "webtest\\screenshots\\elective\\admin_admin_elective.png"
|
||
},
|
||
{
|
||
"url": "http://localhost:3000/admin/elective/create",
|
||
"route": "/admin/elective/create",
|
||
"category": "创建选修课程",
|
||
"status": "passed",
|
||
"http_status": 200,
|
||
"final_url": "http://localhost:3000/admin/elective/create",
|
||
"checks": [
|
||
{
|
||
"name": "标题关键词匹配 (创建课程)",
|
||
"passed": true,
|
||
"detail": "实际匹配: True"
|
||
}
|
||
],
|
||
"errors": [],
|
||
"warnings": [],
|
||
"console_errors": [],
|
||
"screenshot": "webtest\\screenshots\\elective\\admin_admin_elective_create.png"
|
||
}
|
||
],
|
||
"interactions": [
|
||
{
|
||
"name": "管理员选修课 - 创建课程按钮存在",
|
||
"passed": true,
|
||
"detail": ""
|
||
},
|
||
{
|
||
"name": "管理员选修课 - 内容显示(卡片或空状态)",
|
||
"passed": true,
|
||
"detail": ""
|
||
},
|
||
{
|
||
"name": "创建选修课 - 课程名称输入框",
|
||
"passed": true,
|
||
"detail": ""
|
||
},
|
||
{
|
||
"name": "创建选修课 - 选课模式选择器",
|
||
"passed": true,
|
||
"detail": ""
|
||
},
|
||
{
|
||
"name": "创建选修课 - 容量输入框",
|
||
"passed": true,
|
||
"detail": ""
|
||
},
|
||
{
|
||
"name": "创建选修课 - 提交按钮",
|
||
"passed": true,
|
||
"detail": ""
|
||
}
|
||
],
|
||
"errors": [],
|
||
"warnings": []
|
||
},
|
||
"teacher": {
|
||
"role": "teacher",
|
||
"login_success": true,
|
||
"pages": [
|
||
{
|
||
"url": "http://localhost:3000/teacher/elective",
|
||
"route": "/teacher/elective",
|
||
"category": "我的选修课",
|
||
"status": "warning",
|
||
"http_status": 200,
|
||
"final_url": "http://localhost:3000/teacher/elective",
|
||
"checks": [
|
||
{
|
||
"name": "标题关键词匹配 (我的选修课)",
|
||
"passed": true,
|
||
"detail": "实际匹配: True"
|
||
}
|
||
],
|
||
"errors": [],
|
||
"warnings": [
|
||
"控制台错误 1 条"
|
||
],
|
||
"console_errors": [
|
||
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n%s%s https://react.dev/link/hydration-mismatch \n\n ...\n <div className=\"flex items...\">\n <GlobalSearch>\n ...\n <MenuProvider scope={{Menu:[...], ...}} onClose={function Menu.useCallback} isUsingKeyboardRef={{current:false}} ...>\n <DropdownMenuTrigger asChild={true}>\n <DropdownMenuTrigger data-slot=\"dropdown-m...\" asChild={true}>\n <MenuAnchor asChild={true} __scopeMenu={{Menu:[...], ...}}>\n <PopperAnchor __scopePopper={{Menu:[...], ...}} asChild={true} ref={null}>\n <Primitive.div asChild={true} ref={function}>\n <Primitive.div.Slot ref={function}>\n <Primitive.div.SlotClone ref={function}>\n <Primitive.button type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" aria-expanded={false} ...>\n <Primitive.button.Slot type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" ...>\n <Primitive.button.SlotClone type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" ...>\n <Button variant=\"ghost\" size=\"icon\" className=\"relative t...\" aria-label=\"通知\" type=\"button\" ...>\n <button\n data-slot=\"dropdown-menu-trigger\"\n className={\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded...\"}\n aria-label=\"通知\"\n type=\"button\"\n+ id=\"radix-_R_lebn6lb_\"\n- id=\"radix-_R_2lqbn6lb_\"\n aria-haspopup=\"menu\"\n aria-expanded={false}\n aria-controls={undefined}\n data-state=\"closed\"\n data-disabled={undefined}\n disabled={false}\n onPointerDown={function handleEvent}\n onKeyDown={function handleEvent}\n ref={function}\n >\n ...\n ...\n <MenuProvider scope={{Menu:[...], ...}} onClose={function Menu.useCallback} isUsingKeyboardRef={{current:false}} ...>\n <DropdownMenuTrigger asChild={true}>\n <DropdownMenuTrigger data-slot=\"dropdown-m...\" asChild={true}>\n <MenuAnchor asChild={true} __scopeMenu={{Menu:[...], ...}}>\n <PopperAnchor __scopePopper={{Menu:[...], ...}} asChild={true} ref={null}>\n <Primitive.div asChild={true} ref={function}>\n <Primitive.div.Slot ref={function}>\n <Primitive.div.SlotClone ref={function}>\n <Primitive.button type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" aria-expanded={false} ...>\n <Primitive.button.Slot type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" ...>\n <Primitive.button.SlotClone type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" ...>\n <Button variant=\"ghost\" className=\"relative s...\" type=\"button\" id=\"radix-_R_t...\" ...>\n <button\n data-slot=\"dropdown-menu-trigger\"\n className={\"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm...\"}\n type=\"button\"\n+ id=\"radix-_R_tebn6lb_\"\n- id=\"radix-_R_3lqbn6lb_\"\n aria-haspopup=\"menu\"\n aria-expanded={false}\n aria-controls={undefined}\n data-state=\"closed\"\n data-disabled={undefined}\n disabled={false}\n onPointerDown={function handleEvent}\n onKeyDown={function handleEvent}\n ref={function}\n >\n ...\n"
|
||
],
|
||
"screenshot": "webtest\\screenshots\\elective\\teacher_teacher_elective.png"
|
||
}
|
||
],
|
||
"interactions": [
|
||
{
|
||
"name": "教师选修课 - 创建课程按钮存在",
|
||
"passed": true,
|
||
"detail": ""
|
||
},
|
||
{
|
||
"name": "教师选修课 - 内容显示(卡片或空状态)",
|
||
"passed": true,
|
||
"detail": ""
|
||
}
|
||
],
|
||
"errors": [],
|
||
"warnings": []
|
||
},
|
||
"student": {
|
||
"role": "student",
|
||
"login_success": true,
|
||
"pages": [
|
||
{
|
||
"url": "http://localhost:3000/student/elective",
|
||
"route": "/student/elective",
|
||
"category": "选课中心",
|
||
"status": "passed",
|
||
"http_status": 200,
|
||
"final_url": "http://localhost:3000/student/elective",
|
||
"checks": [
|
||
{
|
||
"name": "标题关键词匹配 (选课中心)",
|
||
"passed": true,
|
||
"detail": "实际匹配: True"
|
||
}
|
||
],
|
||
"errors": [],
|
||
"warnings": [],
|
||
"console_errors": [],
|
||
"screenshot": "webtest\\screenshots\\elective\\student_student_elective.png"
|
||
}
|
||
],
|
||
"interactions": [
|
||
{
|
||
"name": "学生选课 - 我的选课区域",
|
||
"passed": true,
|
||
"detail": ""
|
||
},
|
||
{
|
||
"name": "学生选课 - 可选课程区域",
|
||
"passed": true,
|
||
"detail": ""
|
||
},
|
||
{
|
||
"name": "学生选课 - 搜索筛选器",
|
||
"passed": true,
|
||
"detail": ""
|
||
}
|
||
],
|
||
"errors": [],
|
||
"warnings": []
|
||
},
|
||
"parent": {
|
||
"role": "parent",
|
||
"login_success": true,
|
||
"pages": [],
|
||
"interactions": [],
|
||
"errors": [],
|
||
"warnings": []
|
||
}
|
||
},
|
||
"cross_role_tests": [
|
||
{
|
||
"role": "teacher",
|
||
"forbidden_route": "/admin/elective",
|
||
"final_url": "http://localhost:3000/teacher/dashboard?from=%2Fadmin%2Felective&reason=forbidden",
|
||
"passed": true,
|
||
"error": "重定向回 /teacher/dashboard(拒绝访问)"
|
||
},
|
||
{
|
||
"role": "teacher",
|
||
"forbidden_route": "/student/elective",
|
||
"final_url": "http://localhost:3000/teacher/dashboard?from=%2Fstudent%2Felective&reason=forbidden",
|
||
"passed": true,
|
||
"error": "重定向回 /teacher/dashboard(拒绝访问)"
|
||
},
|
||
{
|
||
"role": "student",
|
||
"forbidden_route": "/admin/elective",
|
||
"final_url": "http://localhost:3000/student/dashboard?from=%2Fadmin%2Felective&reason=forbidden",
|
||
"passed": true,
|
||
"error": "重定向回 /student/dashboard(拒绝访问)"
|
||
},
|
||
{
|
||
"role": "student",
|
||
"forbidden_route": "/teacher/elective",
|
||
"final_url": "http://localhost:3000/student/dashboard?from=%2Fteacher%2Felective&reason=forbidden",
|
||
"passed": true,
|
||
"error": "重定向回 /student/dashboard(拒绝访问)"
|
||
},
|
||
{
|
||
"role": "parent",
|
||
"forbidden_route": "/admin/elective",
|
||
"final_url": "http://localhost:3000/parent/dashboard?from=%2Fadmin%2Felective&reason=forbidden",
|
||
"passed": true,
|
||
"error": "重定向回 /parent/dashboard(拒绝访问)"
|
||
},
|
||
{
|
||
"role": "parent",
|
||
"forbidden_route": "/teacher/elective",
|
||
"final_url": "http://localhost:3000/parent/dashboard?from=%2Fteacher%2Felective&reason=forbidden",
|
||
"passed": true,
|
||
"error": "重定向回 /parent/dashboard(拒绝访问)"
|
||
},
|
||
{
|
||
"role": "parent",
|
||
"forbidden_route": "/student/elective",
|
||
"final_url": "http://localhost:3000/parent/dashboard?from=%2Fstudent%2Felective&reason=forbidden",
|
||
"passed": true,
|
||
"error": "重定向回 /parent/dashboard(拒绝访问)"
|
||
}
|
||
],
|
||
"interactions": [],
|
||
"console_errors_global": [
|
||
{
|
||
"role": "teacher",
|
||
"error": "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n%s%s https://react.dev/link/hydration-mismatch \n\n ...\n <div className=\"flex items...\">\n <GlobalSearch>\n ...\n <MenuProvider scope={{Menu:[...], ...}} onClose={function Menu.useCallback} isUsingKeyboardRef={{current:false}} ...>\n <DropdownMenuTrigger asChild={true}>\n <DropdownMenuTrigger data-slot=\"dropdown-m...\" asChild={true}>\n <MenuAnchor asChild={true} __scopeMenu={{Menu:[...], ...}}>\n <PopperAnchor __scopePopper={{Menu:[...], ...}} asChild={true} ref={null}>\n <Primitive.div asChild={true} ref={function}>\n <Primitive.div.Slot ref={function}>\n <Primitive.div.SlotClone ref={function}>\n <Primitive.button type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" aria-expanded={false} ...>\n <Primitive.button.Slot type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" ...>\n <Primitive.button.SlotClone type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" ...>\n <Button variant=\"ghost\" size=\"icon\" className=\"relative t...\" aria-label=\"通知\" type=\"button\" ...>\n <button\n data-slot=\"dropdown-menu-trigger\"\n className={\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded...\"}\n aria-label=\"通知\"\n type=\"button\"\n+ id=\"radix-_R_lebn6lb_\"\n- id=\"radix-_R_2lqbn6lb_\"\n aria-haspopup=\"menu\"\n aria-expanded={false}\n aria-controls={undefined}\n data-state=\"closed\"\n data-disabled={undefined}\n disabled={false}\n onPointerDown={function handleEvent}\n onKeyDown={function handleEvent}\n ref={function}\n >\n ...\n ...\n <MenuProvider scope={{Menu:[...], ...}} onClose={function Menu.useCallback} isUsingKeyboardRef={{current:false}} ...>\n <DropdownMenuTrigger asChild={true}>\n <DropdownMenuTrigger data-slot=\"dropdown-m...\" asChild={true}>\n <MenuAnchor asChild={true} __scopeMenu={{Menu:[...], ...}}>\n <PopperAnchor __scopePopper={{Menu:[...], ...}} asChild={true} ref={null}>\n <Primitive.div asChild={true} ref={function}>\n <Primitive.div.Slot ref={function}>\n <Primitive.div.SlotClone ref={function}>\n <Primitive.button type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" aria-expanded={false} ...>\n <Primitive.button.Slot type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" ...>\n <Primitive.button.SlotClone type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" ...>\n <Button variant=\"ghost\" className=\"relative s...\" type=\"button\" id=\"radix-_R_t...\" ...>\n <button\n data-slot=\"dropdown-menu-trigger\"\n className={\"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm...\"}\n type=\"button\"\n+ id=\"radix-_R_tebn6lb_\"\n- id=\"radix-_R_3lqbn6lb_\"\n aria-haspopup=\"menu\"\n aria-expanded={false}\n aria-controls={undefined}\n data-state=\"closed\"\n data-disabled={undefined}\n disabled={false}\n onPointerDown={function handleEvent}\n onKeyDown={function handleEvent}\n ref={function}\n >\n ...\n"
|
||
},
|
||
{
|
||
"role": "student",
|
||
"error": "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n%s%s https://react.dev/link/hydration-mismatch \n\n ...\n <div className=\"flex items...\">\n <GlobalSearch>\n ...\n <MenuProvider scope={{Menu:[...], ...}} onClose={function Menu.useCallback} isUsingKeyboardRef={{current:false}} ...>\n <DropdownMenuTrigger asChild={true}>\n <DropdownMenuTrigger data-slot=\"dropdown-m...\" asChild={true}>\n <MenuAnchor asChild={true} __scopeMenu={{Menu:[...], ...}}>\n <PopperAnchor __scopePopper={{Menu:[...], ...}} asChild={true} ref={null}>\n <Primitive.div asChild={true} ref={function}>\n <Primitive.div.Slot ref={function}>\n <Primitive.div.SlotClone ref={function}>\n <Primitive.button type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" aria-expanded={false} ...>\n <Primitive.button.Slot type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" ...>\n <Primitive.button.SlotClone type=\"button\" id=\"radix-_R_l...\" aria-haspopup=\"menu\" ...>\n <Button variant=\"ghost\" size=\"icon\" className=\"relative t...\" aria-label=\"通知\" type=\"button\" ...>\n <button\n data-slot=\"dropdown-menu-trigger\"\n className={\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded...\"}\n aria-label=\"通知\"\n type=\"button\"\n+ id=\"radix-_R_lebn6lb_\"\n- id=\"radix-_R_2lqbn6lb_\"\n aria-haspopup=\"menu\"\n aria-expanded={false}\n aria-controls={undefined}\n data-state=\"closed\"\n data-disabled={undefined}\n disabled={false}\n onPointerDown={function handleEvent}\n onKeyDown={function handleEvent}\n ref={function}\n >\n ...\n ...\n <MenuProvider scope={{Menu:[...], ...}} onClose={function Menu.useCallback} isUsingKeyboardRef={{current:false}} ...>\n <DropdownMenuTrigger asChild={true}>\n <DropdownMenuTrigger data-slot=\"dropdown-m...\" asChild={true}>\n <MenuAnchor asChild={true} __scopeMenu={{Menu:[...], ...}}>\n <PopperAnchor __scopePopper={{Menu:[...], ...}} asChild={true} ref={null}>\n <Primitive.div asChild={true} ref={function}>\n <Primitive.div.Slot ref={function}>\n <Primitive.div.SlotClone ref={function}>\n <Primitive.button type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" aria-expanded={false} ...>\n <Primitive.button.Slot type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" ...>\n <Primitive.button.SlotClone type=\"button\" id=\"radix-_R_t...\" aria-haspopup=\"menu\" ...>\n <Button variant=\"ghost\" className=\"relative s...\" type=\"button\" id=\"radix-_R_t...\" ...>\n <button\n data-slot=\"dropdown-menu-trigger\"\n className={\"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm...\"}\n type=\"button\"\n+ id=\"radix-_R_tebn6lb_\"\n- id=\"radix-_R_3lqbn6lb_\"\n aria-haspopup=\"menu\"\n aria-expanded={false}\n aria-controls={undefined}\n data-state=\"closed\"\n data-disabled={undefined}\n disabled={false}\n onPointerDown={function handleEvent}\n onKeyDown={function handleEvent}\n ref={function}\n >\n ...\n"
|
||
}
|
||
]
|
||
} |