{
  "test_date": "2026-06-22 19:57:34",
  "module": "备课 (lesson-preparation)",
  "version": "v1",
  "base_url": "http://localhost:3000",
  "summary": {
    "total": 4,
    "passed": 2,
    "failed": 0,
    "warnings": 2
  },
  "roles": {
    "admin": {
      "role": "admin",
      "login_success": true,
      "list_page": {
        "status": "passed",
        "http_status": 200,
        "final_url": "http://localhost:3000/teacher/lesson-plans",
        "errors": [],
        "warnings": []
      },
      "new_page": {
        "status": "passed",
        "http_status": 200,
        "final_url": "http://localhost:3000/teacher/lesson-plans/new",
        "errors": [],
        "warnings": []
      },
      "checks": [
        {
          "name": "有权访问备课列表页",
          "passed": true,
          "detail": "HTTP 200"
        }
      ],
      "screenshots": [
        "access_admin_list.png",
        "access_admin_new.png"
      ]
    },
    "teacher": {
      "role": "teacher",
      "login_success": true,
      "list_page": {
        "status": "warning",
        "http_status": 200,
        "final_url": "http://localhost:3000/teacher/lesson-plans",
        "errors": [],
        "warnings": [
          "控制台错误 1 条"
        ]
      },
      "new_page": {
        "status": "passed",
        "http_status": 200,
        "final_url": "http://localhost:3000/teacher/lesson-plans/new",
        "errors": [],
        "warnings": []
      },
      "checks": [
        {
          "name": "有权访问备课列表页",
          "passed": true,
          "detail": "HTTP 200"
        }
      ],
      "screenshots": [
        "access_teacher_list.png",
        "access_teacher_new.png"
      ]
    },
    "student": {
      "role": "student",
      "login_success": true,
      "list_page": {
        "status": "passed",
        "http_status": 200,
        "final_url": "http://localhost:3000/student/dashboard?from=%2Fteacher%2Flesson-plans&reason=forbidden",
        "errors": [],
        "warnings": []
      },
      "new_page": {
        "status": "unknown",
        "http_status": null,
        "final_url": null,
        "errors": [],
        "warnings": []
      },
      "checks": [
        {
          "name": "无权限访问被正确拦截",
          "passed": true,
          "detail": "重定向到 http://localhost:3000/student/dashboard?from=%2Fteacher%2Flesson-plans&reason=forbidden"
        }
      ],
      "screenshots": [
        "access_student_list.png"
      ]
    },
    "parent": {
      "role": "parent",
      "login_success": true,
      "list_page": {
        "status": "warning",
        "http_status": 200,
        "final_url": "http://localhost:3000/parent/dashboard?from=%2Fteacher%2Flesson-plans&reason=forbidden",
        "errors": [],
        "warnings": [
          "控制台错误 1 条"
        ]
      },
      "new_page": {
        "status": "unknown",
        "http_status": null,
        "final_url": null,
        "errors": [],
        "warnings": []
      },
      "checks": [
        {
          "name": "无权限访问被正确拦截",
          "passed": true,
          "detail": "重定向到 http://localhost:3000/parent/dashboard?from=%2Fteacher%2Flesson-plans&reason=forbidden"
        }
      ],
      "screenshots": [
        "access_parent_list.png"
      ]
    }
  },
  "console_errors_global": [
    {
      "role": "current",
      "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": "current",
      "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"
    }
  ],
  "teacher_features": {
    "role": "teacher",
    "features": {
      "list": {
        "name": "列表页",
        "status": "passed",
        "checks": [
          {
            "name": "页面标题包含'备课'/'Lesson Plan'",
            "passed": true,
            "detail": "body 长度 380020"
          },
          {
            "name": "存在'新建课案'按钮",
            "passed": true,
            "detail": "找到 1 个"
          },
          {
            "name": "存在筛选器",
            "passed": true,
            "detail": "找到 4 个 input/select"
          },
          {
            "name": "课案卡片或空状态",
            "passed": true,
            "detail": "找到 12 个课案链接"
          }
        ],
        "errors": []
      },
      "create": {
        "name": "新建课案",
        "status": "passed",
        "checks": [
          {
            "name": "存在模板选择按钮",
            "passed": true,
            "detail": "找到 11 个按钮"
          },
          {
            "name": "模板选中状态可视化",
            "passed": true,
            "detail": "选中 5 个"
          },
          {
            "name": "创建后跳转到编辑页",
            "passed": true,
            "detail": "planId=gpqmy3bpyts1lpnhomibnzr2"
          }
        ],
        "errors": []
      },
      "editor": {
        "name": "编辑器",
        "status": "passed",
        "checks": [
          {
            "name": "标题输入框存在且有值",
            "passed": false,
            "detail": "标题=''"
          },
          {
            "name": "React Flow 节点渲染",
            "passed": true,
            "detail": "渲染 8 个节点"
          },
          {
            "name": "React Flow 边渲染",
            "passed": true,
            "detail": "渲染 7 条边"
          },
          {
            "name": "存在保存版本按钮",
            "passed": true,
            "detail": "找到 1 个"
          },
          {
            "name": "存在版本历史按钮",
            "passed": true,
            "detail": "找到 2 个"
          },
          {
            "name": "存在添加节点按钮",
            "passed": true,
            "detail": "找到 1 个"
          }
        ],
        "errors": []
      },
      "panel": {
        "name": "节点选中与侧边面板",
        "status": "passed",
        "checks": [
          {
            "name": "点击节点后出现侧边面板",
            "passed": true,
            "detail": "找到编辑元素: True"
          }
        ],
        "errors": []
      },
      "version": {
        "name": "版本历史",
        "status": "passed",
        "checks": [
          {
            "name": "版本历史抽屉打开",
            "passed": false,
            "detail": "找到 0 个 dialog/drawer"
          }
        ],
        "errors": []
      },
      "duplicate_archive": {
        "name": "复制与归档",
        "status": "passed",
        "checks": [
          {
            "name": "列表页存在课案卡片",
            "passed": true,
            "detail": "找到 13 个"
          },
          {
            "name": "存在复制按钮",
            "passed": true,
            "detail": "找到 12 个"
          },
          {
            "name": "存在归档按钮",
            "passed": true,
            "detail": "找到 12 个"
          },
          {
            "name": "复制后课案数量增加",
            "passed": false,
            "detail": "复制前 13 → 复制后 13"
          }
        ],
        "errors": []
      }
    },
    "screenshots": [
      "teacher_feature_list.png",
      "teacher_feature_create.png",
      "teacher_feature_editor.png",
      "teacher_feature_panel.png",
      "teacher_feature_versions.png",
      "teacher_feature_duplicate.png"
    ],
    "errors": [],
    "warnings": []
  }
}