classroom-interaction.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { test, expect } from '../fixtures/base';
  2. import { ClassroomPage } from '../pages/classroom.page';
  3. import { createSettingsStorage } from '../fixtures/test-data/settings';
  4. import { defaultTheme } from '../fixtures/test-data/scene-content';
  5. const TEST_STAGE_ID = 'e2e-test-stage';
  6. const SETTINGS_STORAGE = createSettingsStorage({ sidebarCollapsed: false });
  7. /** Seed IndexedDB with stage + 3 scenes using raw IndexedDB API */
  8. async function seedDatabase(page: import('@playwright/test').Page) {
  9. // Inject settings before navigating so it's available immediately on load
  10. await page.addInitScript((settings) => {
  11. localStorage.setItem('settings-storage', settings);
  12. }, SETTINGS_STORAGE);
  13. // Navigate to home page first — this causes Dexie to open/create the DB at v8
  14. // with the correct schema. We wait for network idle to ensure Dexie is done.
  15. await page.goto('/', { waitUntil: 'networkidle' });
  16. // Now seed data by opening the DB at its current version (no upgrade).
  17. // Opening without a version number returns the current version without triggering
  18. // onupgradeneeded, so we can safely write to the already-initialized schema.
  19. await page.evaluate(
  20. ({ stageId, theme }) => {
  21. return new Promise<void>((resolve, reject) => {
  22. // Open without specifying version — uses current DB version, no upgrade event
  23. const request = indexedDB.open('MAIC-Database');
  24. request.onsuccess = (event) => {
  25. const db = (event.target as IDBOpenDBRequest).result;
  26. const tx = db.transaction(['stages', 'scenes', 'stageOutlines'], 'readwrite');
  27. const now = Date.now();
  28. tx.objectStore('stages').put({
  29. id: stageId,
  30. name: '光合作用',
  31. description: '',
  32. language: 'zh-CN',
  33. style: 'professional',
  34. createdAt: now,
  35. updatedAt: now,
  36. });
  37. // Scene content uses SlideContent shape: { type: 'slide', canvas: Slide }
  38. const makeSlideContent = (title: string, elId: string) => ({
  39. type: 'slide',
  40. canvas: {
  41. id: `slide-${elId}`,
  42. viewportSize: 1000,
  43. viewportRatio: 0.5625,
  44. theme,
  45. elements: [
  46. {
  47. type: 'text',
  48. id: `el-${elId}`,
  49. content: title,
  50. left: 50,
  51. top: 50,
  52. width: 900,
  53. height: 100,
  54. },
  55. ],
  56. },
  57. });
  58. const scenes = [
  59. {
  60. id: 'scene-0',
  61. stageId,
  62. type: 'slide',
  63. title: '基本概念',
  64. order: 0,
  65. content: makeSlideContent('基本概念', '0'),
  66. createdAt: now,
  67. updatedAt: now,
  68. },
  69. {
  70. id: 'scene-1',
  71. stageId,
  72. type: 'slide',
  73. title: '光反应',
  74. order: 1,
  75. content: makeSlideContent('光反应', '1'),
  76. createdAt: now,
  77. updatedAt: now,
  78. },
  79. {
  80. id: 'scene-2',
  81. stageId,
  82. type: 'slide',
  83. title: '暗反应',
  84. order: 2,
  85. content: makeSlideContent('暗反应', '2'),
  86. createdAt: now,
  87. updatedAt: now,
  88. },
  89. ];
  90. for (const scene of scenes) {
  91. tx.objectStore('scenes').put(scene);
  92. }
  93. // Empty outlines = all scenes generated, no pending work
  94. // StageOutlinesRecord requires createdAt + updatedAt
  95. tx.objectStore('stageOutlines').put({
  96. stageId,
  97. outlines: [],
  98. createdAt: now,
  99. updatedAt: now,
  100. });
  101. tx.oncomplete = () => {
  102. db.close();
  103. resolve();
  104. };
  105. tx.onerror = () => reject(tx.error);
  106. };
  107. request.onerror = () => reject(request.error);
  108. });
  109. },
  110. { stageId: TEST_STAGE_ID, theme: defaultTheme },
  111. );
  112. }
  113. test.describe('Classroom Interaction', () => {
  114. test.beforeEach(async ({ page }) => {
  115. await seedDatabase(page);
  116. });
  117. test('loads classroom and switches scenes', async ({ page }) => {
  118. const classroom = new ClassroomPage(page);
  119. await classroom.goto(TEST_STAGE_ID);
  120. await classroom.waitForLoaded();
  121. // Sidebar shows 3 scenes
  122. await expect(classroom.sidebarScenes).toHaveCount(3, { timeout: 10_000 });
  123. // First scene title visible
  124. await expect(classroom.getSceneTitle(0)).toContainText('基本概念');
  125. // Click second scene
  126. await classroom.clickScene(1);
  127. // Verify second scene is now active — heading in the top bar shows the current scene name
  128. await expect(page.getByRole('heading', { name: '光反应' })).toBeVisible();
  129. });
  130. });