{
  "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"
    }
  ]
}