|
1 | | -import { Extension, getExtensionField, Mark, Node } from '@tiptap/core' |
| 1 | +import { Editor, Extension, getExtensionField, Mark, Node } from '@tiptap/core' |
| 2 | +import Document from '@tiptap/extension-document' |
| 3 | +import Paragraph from '@tiptap/extension-paragraph' |
| 4 | +import Text from '@tiptap/extension-text' |
2 | 5 | import { describe, expect, it } from 'vitest' |
3 | 6 |
|
4 | 7 | declare module '@tiptap/core' { |
@@ -416,3 +419,116 @@ describe('extend extensions', () => { |
416 | 419 | }) |
417 | 420 | }) |
418 | 421 | }) |
| 422 | + |
| 423 | +describe('parent/child cleanup on destroy', () => { |
| 424 | + it('should not leak child reference when configure() is called on a singleton', () => { |
| 425 | + const singleton = Extension.create({ |
| 426 | + name: 'testExtension', |
| 427 | + addOptions() { |
| 428 | + return { foo: 'bar' } |
| 429 | + }, |
| 430 | + }) |
| 431 | + |
| 432 | + const configuredExtension = singleton.configure({ foo: 'baz' }) |
| 433 | + |
| 434 | + expect(singleton.child).toBeNull() |
| 435 | + expect(configuredExtension.parent).toBeNull() |
| 436 | + }) |
| 437 | + |
| 438 | + it('should break parent/child chain when editor is destroyed (extend path)', () => { |
| 439 | + const singleton = Extension.create({ |
| 440 | + name: 'testExtension', |
| 441 | + addOptions() { |
| 442 | + return { foo: 'bar' } |
| 443 | + }, |
| 444 | + }) |
| 445 | + |
| 446 | + const childExtension = singleton.extend({ |
| 447 | + addOptions() { |
| 448 | + return { ...this.parent?.(), foo: 'baz' } |
| 449 | + }, |
| 450 | + }) |
| 451 | + |
| 452 | + expect(singleton.child).toBe(childExtension) |
| 453 | + expect(childExtension.parent).toBe(singleton) |
| 454 | + |
| 455 | + const editor = new Editor({ |
| 456 | + element: null, |
| 457 | + extensions: [Document, Paragraph, Text, childExtension], |
| 458 | + }) |
| 459 | + |
| 460 | + editor.destroy() |
| 461 | + |
| 462 | + expect(singleton.child).toBeNull() |
| 463 | + }) |
| 464 | + |
| 465 | + it('should clear forward parent.child links on all extensions after editor.destroy()', () => { |
| 466 | + const singletonA = Extension.create({ |
| 467 | + name: 'extA', |
| 468 | + addOptions() { |
| 469 | + return { value: 'a' } |
| 470 | + }, |
| 471 | + }) |
| 472 | + const singletonB = Extension.create({ |
| 473 | + name: 'extB', |
| 474 | + addOptions() { |
| 475 | + return { value: 'b' } |
| 476 | + }, |
| 477 | + }) |
| 478 | + |
| 479 | + const configuredA = singletonA.configure({ value: 'a-configured' }) |
| 480 | + const childB = singletonB.extend({ name: 'extB-child' }) |
| 481 | + |
| 482 | + const editor = new Editor({ |
| 483 | + element: null, |
| 484 | + extensions: [Document, Paragraph, Text, configuredA, childB], |
| 485 | + }) |
| 486 | + |
| 487 | + const { extensions } = editor.extensionManager |
| 488 | + |
| 489 | + editor.destroy() |
| 490 | + |
| 491 | + extensions.forEach(ext => { |
| 492 | + if (ext.parent?.child === ext) { |
| 493 | + // This should never be true after destroy — the forward link is always broken |
| 494 | + expect(ext.parent.child).toBeNull() |
| 495 | + } |
| 496 | + }) |
| 497 | + }) |
| 498 | + |
| 499 | + it('should break all ancestor child links in a multi-level extend chain after editor.destroy()', () => { |
| 500 | + const root = Extension.create({ |
| 501 | + name: 'root', |
| 502 | + addOptions() { |
| 503 | + return { level: 0 } |
| 504 | + }, |
| 505 | + }) |
| 506 | + |
| 507 | + const child = root.extend({ |
| 508 | + addOptions() { |
| 509 | + return { ...this.parent?.(), level: 1 } |
| 510 | + }, |
| 511 | + }) |
| 512 | + |
| 513 | + const grandchild = child.extend({ |
| 514 | + addOptions() { |
| 515 | + return { ...this.parent?.(), level: 2 } |
| 516 | + }, |
| 517 | + }) |
| 518 | + |
| 519 | + expect(root.child).toBe(child) |
| 520 | + expect(child.child).toBe(grandchild) |
| 521 | + expect(grandchild.parent).toBe(child) |
| 522 | + expect(child.parent).toBe(root) |
| 523 | + |
| 524 | + const editor = new Editor({ |
| 525 | + element: null, |
| 526 | + extensions: [Document, Paragraph, Text, grandchild], |
| 527 | + }) |
| 528 | + |
| 529 | + editor.destroy() |
| 530 | + |
| 531 | + expect(root.child).toBeNull() |
| 532 | + expect(child.child).toBeNull() |
| 533 | + }) |
| 534 | +}) |
0 commit comments