|
| 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