From c01c66d6d5b53c9b56838035f9b1e3fd11534b33 Mon Sep 17 00:00:00 2001 From: Jeroen De Dauw Date: Tue, 14 Apr 2026 00:31:31 +0200 Subject: [PATCH 1/2] Bump version to 4.0.1 Co-Authored-By: Claude Opus 4.6 (1M context) --- doc/RELEASE-NOTES.md | 5 +++++ extension.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/RELEASE-NOTES.md b/doc/RELEASE-NOTES.md index bf17bf7..09b6d83 100644 --- a/doc/RELEASE-NOTES.md +++ b/doc/RELEASE-NOTES.md @@ -1,5 +1,10 @@ These are the release notes for the [SubPageList extension](../README.md). +## Version 4.0.1 (2026-04-14) + +* Fixed fatal error when deleting pages due to `PageDeleteComplete` hook receiving + a `ProperPageIdentity` instead of a `WikiPage` ([#85](https://github.com/ProfessionalWiki/SubPageList/issues/85)) + ## Version 4.0.0 (2026-04-10) * Added support for MediaWiki 1.44 diff --git a/extension.json b/extension.json index be7cc5e..dc94da2 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "SubPageList", - "version": "4.0.0", + "version": "4.0.1", "author": [ "[https://www.entropywins.wtf/mediawiki Jeroen De Dauw]", "[https://professional.wiki/ Professional.Wiki]" From 743c3f09e68319e029ea3575b271afc0cbad89e2 Mon Sep 17 00:00:00 2001 From: Jeroen De Dauw Date: Tue, 14 Apr 2026 00:36:02 +0200 Subject: [PATCH 2/2] Add tests for all cache invalidation hooks Test the PageSaveComplete, PageMoveComplete, and auto-refresh disabled paths, which were previously untested. Also refactor common test setup into shared helpers. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/Unit/SubPageList/SetupTest.php | 106 ++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 19 deletions(-) diff --git a/tests/Unit/SubPageList/SetupTest.php b/tests/Unit/SubPageList/SetupTest.php index 198da57..3f7ed2e 100644 --- a/tests/Unit/SubPageList/SetupTest.php +++ b/tests/Unit/SubPageList/SetupTest.php @@ -39,40 +39,108 @@ public function testRun() { } public function testDeletedPageInvalidatesCache() { - $registeredCallbacks = []; + $callbacks = []; + $spy = $this->newSpyCacheInvalidator(); - $hookContainer = $this->createMock( HookContainer::class ); - $hookContainer->method( 'register' ) - ->willReturnCallback( function ( $name, $callback ) use ( &$registeredCallbacks ) { - $registeredCallbacks[$name] = $callback; - } ); + $this->runSetupWithAutoRefresh( $spy, $callbacks ); - $cacheInvalidator = new class implements CacheInvalidator { - public ?Title $invalidatedTitle = null; + $page = $this->createMock( ProperPageIdentity::class ); + $page->method( 'getNamespace' )->willReturn( NS_MAIN ); + $page->method( 'getDBkey' )->willReturn( 'DeletedPage' ); - public function invalidateCaches( Title $title ): void { - $this->invalidatedTitle = $title; - } - }; + $callbacks['PageDeleteComplete']( $page ); + + $this->assertCount( 1, $spy->invalidatedTitles ); + $this->assertSame( 'DeletedPage', $spy->invalidatedTitles[0]->getDBkey() ); + $this->assertSame( NS_MAIN, $spy->invalidatedTitles[0]->getNamespace() ); + } + + public function testSavedPageInvalidatesCache() { + $callbacks = []; + $spy = $this->newSpyCacheInvalidator(); + + $this->runSetupWithAutoRefresh( $spy, $callbacks ); + + $wikiPage = $this->createMock( \WikiPage::class ); + $wikiPage->method( 'getTitle' )->willReturn( Title::makeTitle( NS_TALK, 'SavedPage' ) ); + + $callbacks['PageSaveComplete']( $wikiPage ); + + $this->assertCount( 1, $spy->invalidatedTitles ); + $this->assertSame( 'SavedPage', $spy->invalidatedTitles[0]->getDBkey() ); + $this->assertSame( NS_TALK, $spy->invalidatedTitles[0]->getNamespace() ); + } + + public function testMovedPageInvalidatesCacheForBothTitles() { + $callbacks = []; + $spy = $this->newSpyCacheInvalidator(); + + $this->runSetupWithAutoRefresh( $spy, $callbacks ); + + $callbacks['PageMoveComplete']( + Title::makeTitle( NS_MAIN, 'OldPage' ), + Title::makeTitle( NS_MAIN, 'NewPage' ) + ); + + $this->assertCount( 2, $spy->invalidatedTitles ); + $this->assertSame( 'OldPage', $spy->invalidatedTitles[0]->getDBkey() ); + $this->assertSame( 'NewPage', $spy->invalidatedTitles[1]->getDBkey() ); + } + + public function testAutoRefreshDisabledDoesNotInvalidateCache() { + $callbacks = []; + $hookContainer = $this->newCapturingHookContainer( $callbacks ); + $spy = $this->newSpyCacheInvalidator(); $extension = $this->createMock( Extension::class ); $extension->method( 'getSettings' ) - ->willReturn( new Settings( [ Settings::AUTO_REFRESH => true ] ) ); + ->willReturn( new Settings( [ Settings::AUTO_REFRESH => false ] ) ); $extension->method( 'getCacheInvalidator' ) - ->willReturn( $cacheInvalidator ); + ->willReturn( $spy ); $setup = new Setup( $extension, $hookContainer, __DIR__ . '/..' ); $setup->run(); $page = $this->createMock( ProperPageIdentity::class ); $page->method( 'getNamespace' )->willReturn( NS_MAIN ); - $page->method( 'getDBkey' )->willReturn( 'TestPage' ); + $page->method( 'getDBkey' )->willReturn( 'SomePage' ); + + $callbacks['PageDeleteComplete']( $page ); + + $this->assertEmpty( $spy->invalidatedTitles ); + } + + private function runSetupWithAutoRefresh( CacheInvalidator $cacheInvalidator, array &$callbacks ): void { + $hookContainer = $this->newCapturingHookContainer( $callbacks ); - $this->assertArrayHasKey( 'PageDeleteComplete', $registeredCallbacks ); - $registeredCallbacks['PageDeleteComplete']( $page ); + $extension = $this->createMock( Extension::class ); + $extension->method( 'getSettings' ) + ->willReturn( new Settings( [ Settings::AUTO_REFRESH => true ] ) ); + $extension->method( 'getCacheInvalidator' ) + ->willReturn( $cacheInvalidator ); - $this->assertSame( 'TestPage', $cacheInvalidator->invalidatedTitle->getDBkey() ); - $this->assertSame( NS_MAIN, $cacheInvalidator->invalidatedTitle->getNamespace() ); + $setup = new Setup( $extension, $hookContainer, __DIR__ . '/..' ); + $setup->run(); + } + + private function newCapturingHookContainer( array &$callbacks ): HookContainer { + $hookContainer = $this->createMock( HookContainer::class ); + $hookContainer->method( 'register' ) + ->willReturnCallback( function ( $name, $callback ) use ( &$callbacks ) { + $callbacks[$name] = $callback; + } ); + return $hookContainer; + } + + private function newSpyCacheInvalidator() { + return new class implements CacheInvalidator { + /** @var Title[] */ + public array $invalidatedTitles = []; + + public function invalidateCaches( Title $title ): void { + $this->invalidatedTitles[] = $title; + } + }; } private function newExtension() {