Skip to content

Commit 4f25f24

Browse files
authored
Merge pull request #44 from Griboedow/copilot/add-playwright-export-tests-category
fix: use MediaWiki\Title\Title explicitly — global \Title alias removed in MW 1.45
2 parents f9a8b79 + 8de519a commit 4f25f24

File tree

8 files changed

+184
-18
lines changed

8 files changed

+184
-18
lines changed

.github/workflows/tests.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,6 @@ jobs:
483483

484484
- name: Run Playwright Web UI screenshot tests
485485
if: always()
486-
continue-on-error: true
487486
working-directory: mediawiki/extensions/PandocUltimateConverter/tests/playwright
488487
env:
489488
MW_BASE_URL: http://localhost:8080

includes/Api/ApiPandocConvert.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use ApiBase;
88
use MediaWiki\Extension\PandocUltimateConverter\PandocConverterService;
99
use MediaWiki\MediaWikiServices;
10+
use MediaWiki\Title\Title;
1011
use Wikimedia\ParamValidator\ParamValidator;
1112

1213
/**
@@ -56,7 +57,7 @@ public function execute(): void {
5657
}
5758

5859
// Validate target page title
59-
$title = \Title::newFromText( $pageName );
60+
$title = Title::newFromText( $pageName );
6061
if ( $title === null ) {
6162
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $pageName ) ] );
6263
}

includes/Api/ApiPandocLlmPolish.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use MediaWiki\Extension\PandocUltimateConverter\LlmPolishService;
99
use MediaWiki\MediaWikiServices;
1010
use MediaWiki\Revision\SlotRecord;
11+
use MediaWiki\Title\Title;
1112
use Wikimedia\ParamValidator\ParamValidator;
1213

1314
/**
@@ -34,7 +35,7 @@ public function execute(): void {
3435
}
3536

3637
// Validate target page title
37-
$title = \Title::newFromText( $pageName );
38+
$title = Title::newFromText( $pageName );
3839
if ( $title === null || !$title->exists() ) {
3940
$this->dieWithError( [ 'apierror-pandocllmpolish-pagenotfound', wfEscapeWikiText( $pageName ) ] );
4041
}

includes/PandocConverterService.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace MediaWiki\Extension\PandocUltimateConverter;
66

77
use MediaWiki\Extension\PandocUltimateConverter\Processors\PandocTextPostprocessor;
8+
use MediaWiki\Title\Title;
89
use MediaWiki\MediaWikiServices;
910
use MediaWiki\Revision\SlotRecord;
1011

@@ -53,7 +54,7 @@ public function __construct( $config, MediaWikiServices $mwServices, $user ) {
5354
* @throws \RuntimeException If the file cannot be found or conversion fails.
5455
*/
5556
public function convertFileToPage( string $fileName, string $pageName, bool $llmPolish = false ): void {
56-
$fileTitle = \Title::newFromTextThrow( $fileName, NS_FILE );
57+
$fileTitle = Title::newFromTextThrow( $fileName, NS_FILE );
5758
$localFile = $this->repoGroup->findFile( $fileTitle );
5859

5960
if ( !$localFile || !$localFile->exists() ) {
@@ -108,7 +109,7 @@ private function savePandocOutput( array $pandocOutput, string $pageName, bool $
108109
}
109110
}
110111

111-
$title = \Title::newFromText( $pageName );
112+
$title = Title::newFromText( $pageName );
112113
$pageUpdater = $this->titleFactory->newFromTitle( $title )->newPageUpdater( $this->user );
113114
$content = new \WikitextContent( $postprocessedText );
114115
$pageUpdater->setContent( SlotRecord::MAIN, $content );

includes/PandocWrapper.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace MediaWiki\Extension\PandocUltimateConverter;
66

77
use MediaWiki\Shell\Shell;
8+
use MediaWiki\Title\Title;
89
use MediaWiki\MediaWikiServices;
910
use MediaWiki\Extension\PandocUltimateConverter\Processors\DOCPreprocessor;
1011
use MediaWiki\Extension\PandocUltimateConverter\Processors\DOCXColorPreprocessor;
@@ -307,7 +308,7 @@ private function uploadFile( string $file, string $baseName ): string
307308
{
308309
$base = wfBaseName( $file );
309310
$filePageName = $baseName . '-' . $base;
310-
$title = \Title::makeTitleSafe( NS_FILE, $filePageName );
311+
$title = Title::makeTitleSafe( NS_FILE, $filePageName );
311312
$image = $this->mwServices->getRepoGroup()->getLocalRepo()->newFile( $title );
312313

313314
$sha1 = \FSFile::getSha1Base36FromPath( $file );

includes/SpecialPages/SpecialPandocExport.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use MediaWiki\Extension\PandocUltimateConverter\PandocWrapper;
1010
use MediaWiki\Html\Html;
1111
use MediaWiki\MediaWikiServices;
12+
use MediaWiki\Title\Title;
1213

1314
/**
1415
* Special page for exporting wiki pages to external document formats (DOCX, ODT, EPUB, …)
@@ -157,7 +158,7 @@ static function ( string $p ): bool {
157158
// Auto-detect categories vs pages and resolve category members.
158159
$pages = [];
159160
foreach ( $items as $itemName ) {
160-
$title = \Title::newFromText( $itemName );
161+
$title = Title::newFromText( $itemName );
161162
if ( $title !== null && $title->getNamespace() === NS_CATEGORY ) {
162163
wfDebugLog( 'PandocUltimateConverter',
163164
'handleExportRequest: resolving category "' . $itemName . '"' );
@@ -318,7 +319,7 @@ public static function extractWikilinkTargets( string $wikitext ): array {
318319
* @return string[] Page titles (main namespace) found in the category tree.
319320
*/
320321
private function getCategoryPages( string $categoryName, array &$visited ): array {
321-
$title = \Title::newFromText( $categoryName, NS_CATEGORY );
322+
$title = Title::newFromText( $categoryName, NS_CATEGORY );
322323
if ( $title === null ) {
323324
return [];
324325
}
@@ -355,7 +356,7 @@ private function getCategoryPages( string $categoryName, array &$visited ): arra
355356
->fetchResultSet();
356357

357358
foreach ( $res as $row ) {
358-
$memberTitle = \Title::newFromID( (int)$row->cl_from );
359+
$memberTitle = Title::newFromID( (int)$row->cl_from );
359360
if ( $memberTitle === null ) {
360361
wfDebugLog( 'PandocUltimateConverter',
361362
'getCategoryPages: cl_from=' . $row->cl_from
@@ -476,7 +477,7 @@ private function runExport( array $pages, string $format, string $workDir ): str
476477

477478
// Expand templates / parser functions while keeping the result as wikitext
478479
// so that {{TemplateName}} and {{#if:…}} are resolved before Pandoc sees them.
479-
$title = \Title::newFromText( $pageName );
480+
$title = Title::newFromText( $pageName );
480481
$wikitext = $parser->preprocess( $wikitext, $title, $parserOptions );
481482

482483
$wikitexts[] = $wikitext;
@@ -654,7 +655,7 @@ private function exportPdfViaLibreOffice(
654655
* @throws \RuntimeException If the page does not exist or contains no wikitext.
655656
*/
656657
private function getPageWikitext( string $pageName ): string {
657-
$title = \Title::newFromText( $pageName );
658+
$title = Title::newFromText( $pageName );
658659
if ( $title === null || !$title->exists() ) {
659660
wfDebugLog( 'PandocUltimateConverter',
660661
'getPageWikitext: page NOT FOUND "' . $pageName . '"'
@@ -703,7 +704,7 @@ private function gatherImages( string $wikitext, string $mediaDir ): void {
703704
. json_encode( array_slice( $candidates, 0, 20 ) ) );
704705

705706
foreach ( $candidates as $rawLink ) {
706-
$title = \Title::newFromText( $rawLink );
707+
$title = Title::newFromText( $rawLink );
707708
if ( $title === null ) {
708709
wfDebugLog( 'PandocUltimateConverter',
709710
'gatherImages: invalid title for link "' . $rawLink . '"' );
@@ -718,7 +719,7 @@ private function gatherImages( string $wikitext, string $mediaDir ): void {
718719
// Media: links point at the same underlying files as File: links.
719720
// Convert to NS_FILE so RepoGroup::findFile() can locate the file.
720721
$fileTitle = $ns === NS_MEDIA
721-
? \Title::makeTitleSafe( NS_FILE, $title->getDBkey() )
722+
? Title::makeTitleSafe( NS_FILE, $title->getDBkey() )
722723
: $title;
723724

724725
if ( $fileTitle === null ) {

includes/SpecialPages/SpecialPandocUltimateConverter.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace MediaWiki\Extension\PandocUltimateConverter\SpecialPages;
66

77
use MediaWiki\Config\Config;
8+
use MediaWiki\Title\Title;
89
use MediaWiki\Context\RequestContext;
910
use MediaWiki\Extension\PandocUltimateConverter\LlmPolishService;
1011
use MediaWiki\Extension\PandocUltimateConverter\PandocWrapper;
@@ -140,7 +141,7 @@ public function execute( $par ): void
140141

141142
private function deleteFile( string $fileName ): void
142143
{
143-
$fileTitle = \Title::newFromTextThrow( $fileName, NS_FILE );
144+
$fileTitle = Title::newFromTextThrow( $fileName, NS_FILE );
144145
$reason = wfMessage( 'pandocultimateconverter-conversion-complete-comment' )->text();
145146

146147
$fileOnDisk = $this->repoGroup->findFile( $fileTitle, [ 'ignoreRedirect' => true ] );
@@ -176,7 +177,7 @@ public function processForm( array $formData ): void
176177
$this->deleteFile( $fileName );
177178
}
178179
}
179-
$this->getOutput()->redirect( \Title::newFromText( $pageName )->getFullURL() );
180+
$this->getOutput()->redirect( Title::newFromText( $pageName )->getFullURL() );
180181
return;
181182
}
182183

@@ -191,7 +192,7 @@ public function processForm( array $formData ): void
191192
);
192193
return;
193194
}
194-
$this->getOutput()->redirect( \Title::newFromText( $pageName )->getFullURL() );
195+
$this->getOutput()->redirect( Title::newFromText( $pageName )->getFullURL() );
195196
}
196197
}
197198

@@ -219,7 +220,7 @@ private function convertPandocOutputToPage( array $pandocOutput, string $pageNam
219220

220221
$postprocessedText = PandocTextPostprocessor::postprocess( $pandocOutput['text'], $imagesVocabulary );
221222

222-
$title = \Title::newFromText( $pageName );
223+
$title = Title::newFromText( $pageName );
223224
$pageUpdater = $this->titleFactory->newFromTitle( $title )->newPageUpdater( $this->user );
224225
$content = new \WikitextContent( $postprocessedText );
225226
$pageUpdater->setContent( SlotRecord::MAIN, $content );
@@ -237,7 +238,7 @@ private function convertUrlToPage( string $sourceUrl, string $pageName ): void
237238

238239
private function convertFileToPage( string $fileName, string $pageName ): void
239240
{
240-
$fileTitle = \Title::newFromTextThrow( $fileName, NS_FILE );
241+
$fileTitle = Title::newFromTextThrow( $fileName, NS_FILE );
241242
$localFile = $this->repoGroup->findFile( $fileTitle );
242243
$filePath = $localFile->getLocalRefPath();
243244

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// @ts-check
2+
3+
/**
4+
* Playwright end-to-end tests for PandocUltimateConverter category export.
5+
*
6+
* These tests verify that Special:PandocExport correctly:
7+
* 1. Exports all pages from a category when a "Category:…" title is requested.
8+
* 2. Excludes pages that are NOT members of the requested category.
9+
*
10+
* Test pages are created via the MediaWiki API in beforeAll so the tests are
11+
* fully self-contained and do not require pre-seeded wiki data.
12+
*
13+
* Environment variables
14+
* ---------------------
15+
* MW_BASE_URL Base URL of the running MediaWiki instance (default: http://localhost:8080)
16+
* MW_ADMIN_USER Admin username (default: admin)
17+
* MW_ADMIN_PASS Admin password (default: adminpassword)
18+
*/
19+
20+
const { test, expect } = require( '@playwright/test' );
21+
22+
const BASE_URL = process.env.MW_BASE_URL || 'http://localhost:8080';
23+
const ADMIN_USER = process.env.MW_ADMIN_USER || 'admin';
24+
const ADMIN_PASS = process.env.MW_ADMIN_PASS || 'adminpassword';
25+
26+
/** Category used for the export test. */
27+
const TEST_CATEGORY = 'PandocExportTestCategory';
28+
29+
/**
30+
* Two pages that belong to the test category.
31+
* Each carries a unique sentinel string so we can assert it appears in the export output.
32+
*/
33+
const CAT_PAGE_1 = {
34+
title: 'PandocCatExportTest1',
35+
sentinel: 'PandocCatExportSentinelAlpha',
36+
};
37+
const CAT_PAGE_2 = {
38+
title: 'PandocCatExportTest2',
39+
sentinel: 'PandocCatExportSentinelBeta',
40+
};
41+
42+
/**
43+
* A page that is NOT in the test category.
44+
* Its sentinel must not appear in a category export.
45+
*/
46+
const NON_CAT_PAGE = {
47+
title: 'PandocNoCatExportTest',
48+
sentinel: 'PandocNoCatExportSentinelGamma',
49+
};
50+
51+
/**
52+
* Log in to MediaWiki as admin.
53+
*
54+
* @param {import('@playwright/test').Page} page
55+
*/
56+
async function login( page ) {
57+
await page.goto( `${ BASE_URL }/index.php?title=Special:UserLogin` );
58+
await page.locator( '#wpName1' ).fill( ADMIN_USER );
59+
await page.locator( '#wpPassword1' ).fill( ADMIN_PASS );
60+
await Promise.all( [
61+
page.waitForNavigation( { waitUntil: 'networkidle', timeout: 30000 } ),
62+
page.locator( '#wpLoginAttempt' ).click(),
63+
] );
64+
}
65+
66+
/**
67+
* Retrieve a CSRF token from the MediaWiki API.
68+
*
69+
* @param {import('@playwright/test').Page} page Logged-in page.
70+
* @returns {Promise<string>}
71+
*/
72+
async function getCsrfToken( page ) {
73+
const resp = await page.request.get(
74+
`${ BASE_URL }/api.php?action=query&meta=tokens&type=csrf&format=json`
75+
);
76+
const body = await resp.json();
77+
return body.query.tokens.csrftoken;
78+
}
79+
80+
/**
81+
* Create (or overwrite) a wiki page via the MediaWiki API.
82+
*
83+
* @param {import('@playwright/test').Page} page Logged-in page.
84+
* @param {string} title Page title.
85+
* @param {string} content Wikitext content.
86+
*/
87+
async function createWikiPage( page, title, content ) {
88+
const token = await getCsrfToken( page );
89+
const resp = await page.request.post( `${ BASE_URL }/api.php`, {
90+
form: {
91+
action: 'edit',
92+
title,
93+
text: content,
94+
token,
95+
format: 'json',
96+
},
97+
} );
98+
const body = await resp.json();
99+
if ( !body.edit || ( body.edit.result !== 'Success' && body.edit.nochange === undefined ) ) {
100+
throw new Error( `Failed to create page "${ title }": ${ JSON.stringify( body ) }` );
101+
}
102+
}
103+
104+
test.describe( 'PandocExport — category export', () => {
105+
106+
// Create test pages once before all tests in this suite.
107+
test.beforeAll( async ( { browser } ) => {
108+
const ctx = await browser.newContext();
109+
const page = await ctx.newPage();
110+
await login( page );
111+
112+
// Two pages belonging to the test category.
113+
await createWikiPage(
114+
page,
115+
CAT_PAGE_1.title,
116+
`= ${ CAT_PAGE_1.title } =\n\n${ CAT_PAGE_1.sentinel }\n\n[[Category:${ TEST_CATEGORY }]]`
117+
);
118+
await createWikiPage(
119+
page,
120+
CAT_PAGE_2.title,
121+
`= ${ CAT_PAGE_2.title } =\n\n${ CAT_PAGE_2.sentinel }\n\n[[Category:${ TEST_CATEGORY }]]`
122+
);
123+
124+
// One page that is NOT in the category.
125+
await createWikiPage(
126+
page,
127+
NON_CAT_PAGE.title,
128+
`= ${ NON_CAT_PAGE.title } =\n\n${ NON_CAT_PAGE.sentinel }`
129+
);
130+
131+
await ctx.close();
132+
} );
133+
134+
test.beforeEach( async ( { page } ) => {
135+
await login( page );
136+
} );
137+
138+
test( 'exports both category pages and excludes non-member pages', async ( { page } ) => {
139+
// Request a plain-text export of the test category.
140+
// The extension resolves "Category:…" page names to their member pages.
141+
const exportUrl = new URL( `${ BASE_URL }/index.php` );
142+
exportUrl.searchParams.set( 'title', 'Special:PandocExport' );
143+
exportUrl.searchParams.set( 'format', 'txt' );
144+
// SpecialPandocExport reads $request->getArray('items'), matching what the
145+
// Vue App.vue frontend also appends as "items[]".
146+
exportUrl.searchParams.append( 'items[]', `Category:${ TEST_CATEGORY }` );
147+
const response = await page.request.get( exportUrl.toString() );
148+
149+
expect( response.status() ).toBe( 200 );
150+
151+
const body = await response.text();
152+
153+
// Both category members must appear in the export output.
154+
expect( body ).toContain( CAT_PAGE_1.sentinel );
155+
expect( body ).toContain( CAT_PAGE_2.sentinel );
156+
157+
// The page outside the category must NOT appear in the export output.
158+
expect( body ).not.toContain( NON_CAT_PAGE.sentinel );
159+
} );
160+
161+
} );

0 commit comments

Comments
 (0)