| 291 | 291 | expect(registrationError?.errorMessage).toContain('is invalid'); |
| 292 | 292 | }); |
| 293 | 293 | }); |
| 294 | + |
| 295 | + test.describe('registerMacroAlias', () => { |
| 296 | + test.describe('valid', () => { |
| 297 | + test('should register an alias for an existing macro', async ({ page }) => { |
| 298 | + const result = await page.evaluate(async () => { |
| 299 | + /** @type {import('../../public/scripts/macros/engine/MacroRegistry.js')} */ |
| 300 | + const { MacroRegistry } = await import('./scripts/macros/engine/MacroRegistry.js'); |
| 301 | + |
| 302 | + // Clean up any existing registrations |
| 303 | + MacroRegistry.unregisterMacro('alias-target'); |
| 304 | + MacroRegistry.unregisterMacro('my-alias'); |
| 305 | + |
| 306 | + // Register target macro |
| 307 | + MacroRegistry.registerMacro('alias-target', { |
| 308 | + description: 'Target macro for alias test', |
| 309 | + handler: () => 'target-result', |
| 310 | + }); |
| 311 | + |
| 312 | + // Register alias |
| 313 | + const success = MacroRegistry.registerMacroAlias('alias-target', 'my-alias'); |
| 314 | + |
| 315 | + const aliasDef = MacroRegistry.getMacro('my-alias'); |
| 316 | + const targetDef = MacroRegistry.getMacro('alias-target'); |
| 317 | + |
| 318 | + return { |
| 319 | + success, |
| 320 | + aliasName: aliasDef?.name, |
| 321 | + aliasOf: aliasDef?.aliasOf, |
| 322 | + aliasVisible: aliasDef?.aliasVisible, |
| 323 | + targetName: targetDef?.name, |
| 324 | + sameHandler: aliasDef?.handler === targetDef?.handler, |
| 325 | + }; |
| 326 | + }); |
| 327 | + |
| 328 | + expect(result.success).toBe(true); |
| 329 | + expect(result.aliasName).toBe('my-alias'); |
| 330 | + expect(result.aliasOf).toBe('alias-target'); |
| 331 | + expect(result.aliasVisible).toBe(true); |
| 332 | + expect(result.targetName).toBe('alias-target'); |
| 333 | + expect(result.sameHandler).toBe(true); |
| 334 | + }); |
| 335 | + |
| 336 | + test('should register alias with visible=false option', async ({ page }) => { |
| 337 | + const result = await page.evaluate(async () => { |
| 338 | + /** @type {import('../../public/scripts/macros/engine/MacroRegistry.js')} */ |
| 339 | + const { MacroRegistry } = await import('./scripts/macros/engine/MacroRegistry.js'); |
| 340 | + |
| 341 | + MacroRegistry.unregisterMacro('alias-target-hidden'); |
| 342 | + MacroRegistry.unregisterMacro('hidden-alias'); |
| 343 | + |
| 344 | + MacroRegistry.registerMacro('alias-target-hidden', { |
| 345 | + description: 'Target macro', |
| 346 | + handler: () => 'result', |
| 347 | + }); |
| 348 | + |
| 349 | + const success = MacroRegistry.registerMacroAlias('alias-target-hidden', 'hidden-alias', { visible: false }); |
| 350 | + const aliasDef = MacroRegistry.getMacro('hidden-alias'); |
| 351 | + |
| 352 | + return { |
| 353 | + success, |
| 354 | + aliasVisible: aliasDef?.aliasVisible, |
| 355 | + }; |
| 356 | + }); |
| 357 | + |
| 358 | + expect(result.success).toBe(true); |
| 359 | + expect(result.aliasVisible).toBe(false); |
| 360 | + }); |
| 361 | + |
| 362 | + test('should resolve alias of alias to primary definition', async ({ page }) => { |
| 363 | + const result = await page.evaluate(async () => { |
| 364 | + /** @type {import('../../public/scripts/macros/engine/MacroRegistry.js')} */ |
| 365 | + const { MacroRegistry } = await import('./scripts/macros/engine/MacroRegistry.js'); |
| 366 | + |
| 367 | + MacroRegistry.unregisterMacro('primary-macro'); |
| 368 | + MacroRegistry.unregisterMacro('first-alias'); |
| 369 | + MacroRegistry.unregisterMacro('second-alias'); |
| 370 | + |
| 371 | + // Register primary macro |
| 372 | + MacroRegistry.registerMacro('primary-macro', { |
| 373 | + description: 'Primary macro', |
| 374 | + handler: () => 'primary-result', |
| 375 | + }); |
| 376 | + |
| 377 | + // Register first alias |
| 378 | + MacroRegistry.registerMacroAlias('primary-macro', 'first-alias'); |
| 379 | + |
| 380 | + // Register alias of alias (should resolve to primary) |
| 381 | + const success = MacroRegistry.registerMacroAlias('first-alias', 'second-alias'); |
| 382 | + |
| 383 | + const secondAliasDef = MacroRegistry.getMacro('second-alias'); |
| 384 | + |
| 385 | + return { |
| 386 | + success, |
| 387 | + aliasOf: secondAliasDef?.aliasOf, |
| 388 | + }; |
| 389 | + }); |
| 390 | + |
| 391 | + expect(result.success).toBe(true); |
| 392 | + // Should point to primary, not to the intermediate alias |
| 393 | + expect(result.aliasOf).toBe('primary-macro'); |
| 394 | + }); |
| 395 | + |
| 396 | + test('should have independent source for alias', async ({ page }) => { |
| 397 | + const result = await page.evaluate(async () => { |
| 398 | + /** @type {import('../../public/scripts/macros/engine/MacroRegistry.js')} */ |
| 399 | + const { MacroRegistry } = await import('./scripts/macros/engine/MacroRegistry.js'); |
| 400 | + |
| 401 | + MacroRegistry.unregisterMacro('source-target'); |
| 402 | + MacroRegistry.unregisterMacro('source-alias'); |
| 403 | + |
| 404 | + MacroRegistry.registerMacro('source-target', { |
| 405 | + description: 'Target', |
| 406 | + handler: () => '', |
| 407 | + }); |
| 408 | + |
| 409 | + MacroRegistry.registerMacroAlias('source-target', 'source-alias'); |
| 410 | + |
| 411 | + const targetDef = MacroRegistry.getMacro('source-target'); |
| 412 | + const aliasDef = MacroRegistry.getMacro('source-alias'); |
| 413 | + |
| 414 | + return { |
| 415 | + // Both should have source objects |
| 416 | + targetHasSource: !!targetDef?.source, |
| 417 | + aliasHasSource: !!aliasDef?.source, |
| 418 | + // The alias has its own source object (not shared reference) |
| 419 | + sourcesAreDifferentObjects: targetDef?.source !== aliasDef?.source, |
| 420 | + }; |
| 421 | + }); |
| 422 | + |
| 423 | + expect(result.targetHasSource).toBe(true); |
| 424 | + expect(result.aliasHasSource).toBe(true); |
| 425 | + expect(result.sourcesAreDifferentObjects).toBe(true); |
| 426 | + }); |
| 427 | + |
| 428 | + test('should be case-insensitive for lookup', async ({ page }) => { |
| 429 | + const result = await page.evaluate(async () => { |
| 430 | + /** @type {import('../../public/scripts/macros/engine/MacroRegistry.js')} */ |
| 431 | + const { MacroRegistry } = await import('./scripts/macros/engine/MacroRegistry.js'); |
| 432 | + |
| 433 | + MacroRegistry.unregisterMacro('case-target'); |
| 434 | + MacroRegistry.unregisterMacro('CaseAlias'); |
| 435 | + |
| 436 | + MacroRegistry.registerMacro('case-target', { |
| 437 | + handler: () => '', |
| 438 | + }); |
| 439 | + |
| 440 | + MacroRegistry.registerMacroAlias('case-target', 'CaseAlias'); |
| 441 | + |
| 442 | + return { |
| 443 | + foundLowercase: !!MacroRegistry.getMacro('casealias'), |
| 444 | + foundUppercase: !!MacroRegistry.getMacro('CASEALIAS'), |
| 445 | + foundMixed: !!MacroRegistry.getMacro('CaseAlias'), |
| 446 | + }; |
| 447 | + }); |
| 448 | + |
| 449 | + expect(result.foundLowercase).toBe(true); |
| 450 | + expect(result.foundUppercase).toBe(true); |
| 451 | + expect(result.foundMixed).toBe(true); |
| 452 | + }); |
| 453 | + }); |
| 454 | + |
| 455 | + test.describe('reject', () => { |
| 456 | + test('should reject invalid alias name', async ({ page }) => { |
| 457 | + const result = await registerAliasAndCaptureErrors(page, { |
| 458 | + targetMacroName: 'random', |
| 459 | + aliasName: '123-invalid', |
| 460 | + }); |
| 461 | + |
| 462 | + expect(result.success).toBe(false); |
| 463 | + expect(result.errors.length).toBeGreaterThan(0); |
| 464 | + |
| 465 | + const registrationError = result.errors.find(e => e.text.includes('[Macro] Registration Error:')); |
| 466 | + expect(registrationError).toBeTruthy(); |
| 467 | + expect(registrationError?.text).toContain('Failed to register alias "123-invalid"'); |
| 468 | + expect(registrationError?.errorMessage).toContain('is invalid'); |
| 469 | + }); |
| 470 | + |
| 471 | + test('should reject alias same as target name (case insensitive)', async ({ page }) => { |
| 472 | + const result = await registerAliasAndCaptureErrors(page, { |
| 473 | + targetMacroName: 'random', |
| 474 | + aliasName: 'RANDOM', |
| 475 | + }); |
| 476 | + |
| 477 | + expect(result.success).toBe(false); |
| 478 | + expect(result.errors.length).toBeGreaterThan(0); |
| 479 | + |
| 480 | + const registrationError = result.errors.find(e => e.text.includes('[Macro] Registration Error:')); |
| 481 | + expect(registrationError).toBeTruthy(); |
| 482 | + expect(registrationError?.errorMessage).toContain('cannot be the same as the target macro name'); |
| 483 | + }); |
| 484 | + |
| 485 | + test('should reject alias for non-existent target macro', async ({ page }) => { |
| 486 | + const result = await registerAliasAndCaptureErrors(page, { |
| 487 | + targetMacroName: 'non-existent-macro-xyz', |
| 488 | + aliasName: 'my-alias', |
| 489 | + }); |
| 490 | + |
| 491 | + expect(result.success).toBe(false); |
| 492 | + expect(result.errors.length).toBeGreaterThan(0); |
| 493 | + |
| 494 | + const registrationError = result.errors.find(e => e.text.includes('[Macro] Registration Error:')); |
| 495 | + expect(registrationError).toBeTruthy(); |
| 496 | + expect(registrationError?.errorMessage).toContain('is not registered'); |
| 497 | + }); |
| 498 | + |
| 499 | + test('should reject alias with special characters', async ({ page }) => { |
| 500 | + const result = await registerAliasAndCaptureErrors(page, { |
| 501 | + targetMacroName: 'random', |
| 502 | + aliasName: 'alias@name', |
| 503 | + }); |
| 504 | + |
| 505 | + expect(result.success).toBe(false); |
| 506 | + const registrationError = result.errors.find(e => e.text.includes('[Macro] Registration Error:')); |
| 507 | + expect(registrationError?.errorMessage).toContain('is invalid'); |
| 508 | + }); |
| 509 | + |
| 510 | + test('should reject alias starting with hyphen', async ({ page }) => { |
| 511 | + const result = await registerAliasAndCaptureErrors(page, { |
| 512 | + targetMacroName: 'random', |
| 513 | + aliasName: '-alias', |
| 514 | + }); |
| 515 | + |
| 516 | + expect(result.success).toBe(false); |
| 517 | + const registrationError = result.errors.find(e => e.text.includes('[Macro] Registration Error:')); |
| 518 | + expect(registrationError?.errorMessage).toContain('is invalid'); |
| 519 | + }); |
| 520 | + }); |
| 521 | + |
| 522 | + test.describe('warnings', () => { |
| 523 | + test('should warn when alias overwrites existing macro', async ({ page }) => { |
| 524 | + const result = await page.evaluate(async () => { |
| 525 | + /** @type {string[]} */ |
| 526 | + const warnings = []; |
| 527 | + const originalWarn = console.warn; |
| 528 | + |
| 529 | + console.warn = (...args) => { |
| 530 | + warnings.push(args.map(a => String(a)).join(' ')); |
| 531 | + }; |
| 532 | + |
| 533 | + try { |
| 534 | + /** @type {import('../../public/scripts/macros/engine/MacroRegistry.js')} */ |
| 535 | + const { MacroRegistry } = await import('./scripts/macros/engine/MacroRegistry.js'); |
| 536 | + |
| 537 | + MacroRegistry.unregisterMacro('overwrite-target'); |
| 538 | + MacroRegistry.unregisterMacro('overwrite-existing'); |
| 539 | + |
| 540 | + // Register target macro |
| 541 | + MacroRegistry.registerMacro('overwrite-target', { |
| 542 | + handler: () => 'target', |
| 543 | + }); |
| 544 | + |
| 545 | + // Register a macro that will be overwritten |
| 546 | + MacroRegistry.registerMacro('overwrite-existing', { |
| 547 | + handler: () => 'existing', |
| 548 | + }); |
| 549 | + |
| 550 | + // Register alias that overwrites existing macro |
| 551 | + const success = MacroRegistry.registerMacroAlias('overwrite-target', 'overwrite-existing'); |
| 552 | + |
| 553 | + return { success, warnings }; |
| 554 | + } finally { |
| 555 | + console.warn = originalWarn; |
| 556 | + } |
| 557 | + }); |
| 558 | + |
| 559 | + expect(result.success).toBe(true); |
| 560 | + const overwriteWarning = result.warnings.find(w => |
| 561 | + w.includes('overwrites an existing macro') && w.includes('overwrite-existing'), |
| 562 | + ); |
| 563 | + expect(overwriteWarning).toBeTruthy(); |
| 564 | + }); |
| 565 | + |
| 566 | + test('should warn when alias overwrites another alias', async ({ page }) => { |
| 567 | + const result = await page.evaluate(async () => { |
| 568 | + /** @type {string[]} */ |
| 569 | + const warnings = []; |
| 570 | + const originalWarn = console.warn; |
| 571 | + |
| 572 | + console.warn = (...args) => { |
| 573 | + warnings.push(args.map(a => String(a)).join(' ')); |
| 574 | + }; |
| 575 | + |
| 576 | + try { |
| 577 | + /** @type {import('../../public/scripts/macros/engine/MacroRegistry.js')} */ |
| 578 | + const { MacroRegistry } = await import('./scripts/macros/engine/MacroRegistry.js'); |
| 579 | + |
| 580 | + MacroRegistry.unregisterMacro('alias-warn-target1'); |
| 581 | + MacroRegistry.unregisterMacro('alias-warn-target2'); |
| 582 | + MacroRegistry.unregisterMacro('shared-alias-name'); |
| 583 | + |
| 584 | + // Register two target macros |
| 585 | + MacroRegistry.registerMacro('alias-warn-target1', { handler: () => '1' }); |
| 586 | + MacroRegistry.registerMacro('alias-warn-target2', { handler: () => '2' }); |
| 587 | + |
| 588 | + // Register first alias |
| 589 | + MacroRegistry.registerMacroAlias('alias-warn-target1', 'shared-alias-name'); |
| 590 | + |
| 591 | + // Clear warnings from first registration |
| 592 | + warnings.length = 0; |
| 593 | + |
| 594 | + // Register second alias with same name (should warn) |
| 595 | + MacroRegistry.registerMacroAlias('alias-warn-target2', 'shared-alias-name'); |
| 596 | + |
| 597 | + return { warnings }; |
| 598 | + } finally { |
| 599 | + console.warn = originalWarn; |
| 600 | + } |
| 601 | + }); |
| 602 | + |
| 603 | + const overwriteWarning = result.warnings.find(w => |
| 604 | + w.includes('overwrites an existing macro') && w.includes('shared-alias-name'), |
| 605 | + ); |
| 606 | + expect(overwriteWarning).toBeTruthy(); |
| 607 | + }); |
| 608 | + }); |
| 609 | + }); |
| 294 | 610 | }); |
| 295 | 611 | |
| 296 | 612 | /** |