test-metadata-viewer.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import fs from 'fs';
  2. import path from 'path';
  3. const html = fs.readFileSync(path.join(process.cwd(), 'public/metadata-viewer.html'), 'utf8');
  4. const scriptMatch = html.match(/<script>([\s\S]*)<\/script>/);
  5. if (!scriptMatch) {
  6. console.error('Cannot extract <script> from public/metadata-viewer.html');
  7. process.exit(1);
  8. }
  9. const jsCode = scriptMatch[1];
  10. function createNode(id) {
  11. return {
  12. id,
  13. dataset: {},
  14. style: { display: '', minWidth: '', minHeight: '' },
  15. innerHTML: '',
  16. textContent: '',
  17. children: [],
  18. className: '',
  19. onclick: null,
  20. onmouseenter: null,
  21. onmouseleave: null,
  22. appendChild(child) { this.children.push(child); return child; },
  23. setAttribute() {},
  24. getAttribute() { return null; },
  25. querySelectorAll() { return []; },
  26. };
  27. }
  28. const elements = {};
  29. function $(id) {
  30. if (!elements[id]) elements[id] = createNode(id);
  31. return elements[id];
  32. }
  33. global.document = {
  34. getElementById: (id) => $(id),
  35. createElement: (tag) => createNode(`${tag}-${Math.random()}`),
  36. createElementNS: (ns, tag) => createNode(`${tag}-${Math.random()}`),
  37. };
  38. global.window = {
  39. parent: { postMessage() {} },
  40. addEventListener() {},
  41. removeEventListener() {},
  42. };
  43. global.addEventListener = () => {};
  44. eval(`(function(){
  45. ${jsCode}
  46. global._metadataViewer = {
  47. parseMeta,
  48. getState: () => ({ nodes, connections }),
  49. };
  50. })()`);
  51. let passed = 0;
  52. let failed = 0;
  53. function test(name, fn) {
  54. try {
  55. fn();
  56. console.log(` ✓ ${name}`);
  57. passed++;
  58. } catch (e) {
  59. console.log(` ✗ ${name}: ${e.message}`);
  60. failed++;
  61. }
  62. }
  63. function assert(cond, msg) {
  64. if (!cond) throw new Error(msg || 'Assertion failed');
  65. }
  66. function getCounts() {
  67. const { nodes, connections } = global._metadataViewer.getState();
  68. const counts = nodes.reduce((acc, node) => {
  69. acc[node.type] = (acc[node.type] || 0) + 1;
  70. return acc;
  71. }, {});
  72. return { nodes, connections, counts };
  73. }
  74. console.log('\n── Metadata Viewer Regression ──');
  75. test('renders legacy database.tables even before apps/services exist', () => {
  76. global._metadataViewer.parseMeta({
  77. project: { name: 'PartialLegacy' },
  78. database: {
  79. name: 'PartialLegacy',
  80. tables: [
  81. { name: 'Courses', fields: [{ name: 'title', type: 'STRING' }] },
  82. { name: 'PrepNotes', fields: [{ name: 'courseId', type: 'INT' }] },
  83. ],
  84. },
  85. serviceDomains: [],
  86. components: [],
  87. sections: [],
  88. apps: [],
  89. });
  90. const { counts } = getCounts();
  91. assert(counts.table === 2, `Expected 2 table nodes, got ${counts.table || 0}`);
  92. });
  93. test('uses legacy sourceTable for VT labels and table connections', () => {
  94. global._metadataViewer.parseMeta({
  95. project: { name: 'LegacySourceTable' },
  96. database: {
  97. name: 'LegacySourceTable',
  98. tables: [
  99. { name: 'Courses', fields: [{ name: 'title', type: 'STRING' }] },
  100. ],
  101. },
  102. serviceDomains: [
  103. {
  104. domainId: 'Course',
  105. virtualTables: [
  106. {
  107. name: 'CourseList',
  108. sourceTable: 'Courses',
  109. fields: [{ name: 'title', type: 'STRING' }],
  110. },
  111. ],
  112. services: [
  113. { serviceId: 'Course.GetCourseList', name: 'GetCourseList' },
  114. ],
  115. },
  116. ],
  117. components: [],
  118. sections: [],
  119. apps: [],
  120. });
  121. const { nodes, connections, counts } = getCounts();
  122. const vtNode = nodes.find((node) => node.id === 'Course.CourseList');
  123. assert(vtNode, 'Expected Course.CourseList VT node');
  124. assert(vtNode.sub === 'source: Courses', `Expected VT source label, got ${vtNode.sub}`);
  125. assert(counts.table === 1, `Expected 1 table node, got ${counts.table || 0}`);
  126. assert(connections.some((conn) => conn.from === 'Course.CourseList' && conn.to === 'Courses' && conn.type === 'data'),
  127. 'Expected VT → table data connection');
  128. });
  129. test('connects app page sectionRefs and section servicesUsed from extracted metadata', () => {
  130. global._metadataViewer.parseMeta({
  131. project: { name: 'TeacherPrep' },
  132. apps: [
  133. {
  134. appId: 'TeacherPrepApp',
  135. pages: [
  136. {
  137. pageId: 'Main',
  138. sectionRefs: [
  139. { sectionId: 'NavSidebar', instanceId: 'navSidebar' },
  140. { sectionId: 'DashboardMain', instanceId: 'dashboardMain' },
  141. ],
  142. componentRefs: [
  143. { componentId: 'ConfirmDialog', instanceId: 'confirmDialog' },
  144. ],
  145. routeMap: {
  146. dashboard: 'DashboardMain',
  147. },
  148. },
  149. ],
  150. },
  151. ],
  152. sections: [
  153. {
  154. sectionId: 'DashboardMain',
  155. servicesUsed: [
  156. {
  157. domainId: 'Dashboard',
  158. services: [
  159. { serviceId: 'Dashboard.GetStats', serviceName: 'GetStats' },
  160. ],
  161. },
  162. ],
  163. },
  164. { sectionId: 'NavSidebar' },
  165. ],
  166. components: [
  167. { componentId: 'ConfirmDialog' },
  168. ],
  169. services: [
  170. {
  171. domainId: 'Dashboard',
  172. methods: [
  173. { name: 'GetStats' },
  174. ],
  175. },
  176. ],
  177. dataSchema: { tables: [] },
  178. });
  179. const { connections } = getCounts();
  180. assert(connections.some((conn) => conn.from === 'Main' && conn.to === 'DashboardMain' && conn.type === 'ui'),
  181. 'Expected page → section connection from sectionRefs/routeMap');
  182. assert(connections.some((conn) => conn.from === 'Main' && conn.to === 'ConfirmDialog' && conn.type === 'comp'),
  183. 'Expected page → component connection from componentRefs');
  184. assert(connections.some((conn) => conn.from === 'DashboardMain' && conn.to === 'Dashboard.GetStats' && conn.type === 'service'),
  185. 'Expected section → service connection from servicesUsed');
  186. });
  187. console.log(`\n── Results ──\n\n ${passed} passed, ${failed} failed\n`);
  188. process.exit(failed > 0 ? 1 : 0);