Skip to content

Commit 0d8ca60

Browse files
committed
Add Playwright E2E tests for admin link correction
1 parent cb433c5 commit 0d8ca60

File tree

8 files changed

+235
-1
lines changed

8 files changed

+235
-1
lines changed

.github/workflows/frontend-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ jobs:
1818
run: npm ci
1919
- name: Run front-end tests
2020
run: npm test
21+
- name: Install Playwright browsers
22+
run: npx playwright install --with-deps chromium
23+
- name: Run end-to-end tests
24+
run: npm run test:e2e

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
/vendor/
22
/tmp/
33
/node_modules/
4+
.playwright/
5+
playwright-report/
6+
test-results/

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@ Liens Morts Detector est une extension WordPress qui détecte les liens et image
5454
- La taille des lots analysés peut être ajustée pour s’adapter aux capacités de l’hébergement (de manière optionnelle via l’interface ou un filtre).
5555
- L’analyse des images distantes (CDN, sous-domaines médias) peut être activée dans les réglages. Cette vérification reste basée sur les fichiers présents dans `wp-content/uploads` et peut rallonger la durée du scan ou consommer davantage de quotas côté CDN.
5656

57+
## Tests automatisés
58+
59+
### JavaScript (Jest)
60+
- `npm test` exécute l’intégralité de la suite Jest existante.
61+
62+
### End-to-end (Playwright)
63+
1. Installer les navigateurs Playwright une fois : `npx playwright install --with-deps chromium`.
64+
2. Exporter les variables d’environnement nécessaires à l’authentification WordPress :
65+
- `WP_E2E_BASE_URL` : URL racine de l’installation locale (`https://wp.test` par exemple).
66+
- `WP_E2E_USERNAME` et `WP_E2E_PASSWORD` : identifiants d’un compte ayant accès au back-office.
67+
- `WP_E2E_SAMPLE_LINK` : URL cassée présente dans le rapport et utilisée par le test.
68+
- `WP_E2E_REPLACEMENT_URL` (optionnel) : URL de remplacement à appliquer pendant le scénario.
69+
- `WP_E2E_STORAGE_STATE` (optionnel) : chemin du fichier de session Playwright si vous ne souhaitez pas utiliser la valeur par défaut `.playwright/wp-admin-state.json`.
70+
3. Lancer `npm run test:e2e` pour rejouer le parcours de correction d’un lot.
71+
72+
> 💡 Lorsqu’elles sont définies, les variables `WP_E2E_*` permettent également à la configuration Playwright de générer automatiquement un état de session réutilisable via `tests/e2e/utils/global-setup.ts`. À défaut de configuration, la suite E2E est ignorée (et renvoie un succès) ce qui permet son exécution dans la CI même sans instance WordPress accessible.
73+
74+
### Combinaison des suites
75+
- `npm run test:all` exécute successivement Jest puis Playwright.
76+
5777
## Détection des soft 404
5878

5979
### Principe des heuristiques

package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
"version": "1.0.0",
44
"private": true,
55
"scripts": {
6-
"test": "jest --runInBand"
6+
"test": "jest --runInBand",
7+
"test:e2e": "playwright test",
8+
"test:all": "npm run test && npm run test:e2e"
79
},
810
"devDependencies": {
911
"@babel/core": "^7.24.0",
12+
"@playwright/test": "^1.56.0",
1013
"@wordpress/jest-preset-default": "^12.31.0",
1114
"babel-jest": "^30.2.0",
1215
"jest": "^30.2.0",

playwright.config.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
5+
const baseURL = process.env.WP_E2E_BASE_URL ?? 'http://localhost:8889';
6+
const storageStatePath = process.env.WP_E2E_STORAGE_STATE ?? path.resolve(__dirname, '.playwright', 'wp-admin-state.json');
7+
const hasStorageState = fs.existsSync(storageStatePath);
8+
9+
const reporters = process.env.CI
10+
? [['github'], ['html', { open: 'never' }], ['list']]
11+
: [['list'], ['html', { open: 'never' }]];
12+
13+
export default defineConfig({
14+
testDir: path.resolve(__dirname, 'tests/e2e'),
15+
timeout: 120_000,
16+
expect: {
17+
timeout: 15_000,
18+
},
19+
reporter: reporters,
20+
retries: process.env.CI ? 1 : 0,
21+
workers: process.env.CI ? 2 : undefined,
22+
use: {
23+
baseURL,
24+
headless: true,
25+
storageState: hasStorageState ? storageStatePath : undefined,
26+
trace: 'on-first-retry',
27+
screenshot: 'only-on-failure',
28+
video: 'retain-on-failure',
29+
},
30+
projects: [
31+
{
32+
name: 'chromium',
33+
use: { ...devices['Desktop Chrome'] },
34+
},
35+
],
36+
globalSetup: require.resolve('./tests/e2e/utils/global-setup'),
37+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
const baseURL = process.env.WP_E2E_BASE_URL;
4+
const username = process.env.WP_E2E_USERNAME;
5+
const password = process.env.WP_E2E_PASSWORD;
6+
const targetLink = process.env.WP_E2E_SAMPLE_LINK;
7+
const replacementFallback = targetLink ? `${targetLink}-corrige` : 'https://example.com/lien-corrige';
8+
const replacementUrl = process.env.WP_E2E_REPLACEMENT_URL ?? replacementFallback;
9+
10+
const shouldSkip = !baseURL || !username || !password || !targetLink;
11+
12+
test.describe('Correction d\'un lot de liens cassés', () => {
13+
test.skip(shouldSkip, 'Les variables WP_E2E_* doivent être définies pour exécuter les tests end-to-end.');
14+
15+
test('permet de modifier un lien cassé depuis le tableau des liens', async ({ page }) => {
16+
await page.goto('/wp-admin/admin.php?page=blc-dashboard', { waitUntil: 'domcontentloaded' });
17+
18+
if (page.url().includes('wp-login.php')) {
19+
await page.fill('input#user_login', username!);
20+
await page.fill('input#user_pass', password!);
21+
await Promise.all([
22+
page.waitForURL(/\/wp-admin\//, { timeout: 60_000 }),
23+
page.click('input#wp-submit'),
24+
]);
25+
await page.goto('/wp-admin/admin.php?page=blc-dashboard', { waitUntil: 'domcontentloaded' });
26+
}
27+
28+
const tableRegion = page.locator('[data-blc-table-region]');
29+
await expect(tableRegion, 'Le tableau des liens cassés doit être visible.').toBeVisible();
30+
31+
const targetRow = page.locator('#the-list tr', { hasText: targetLink! }).first();
32+
await expect(targetRow, 'Le lien attendu doit être présent avant la correction.').toBeVisible();
33+
34+
await test.step('Ouvrir la modale d\'édition', async () => {
35+
await targetRow.locator('.blc-edit-link').click();
36+
await expect(page.locator('#blc-modal')).toBeVisible();
37+
});
38+
39+
await test.step('Soumettre la nouvelle URL', async () => {
40+
const modal = page.locator('#blc-modal');
41+
await modal.locator('.blc-modal__input').fill(replacementUrl);
42+
await Promise.all([
43+
page.waitForResponse((response) =>
44+
response.url().includes('admin-ajax.php') && response.request().method() === 'POST'
45+
),
46+
modal.getByRole('button', { name: 'Mettre à jour' }).click(),
47+
]);
48+
});
49+
50+
await test.step('Vérifier que la ligne est mise à jour', async () => {
51+
await expect(
52+
page.locator('#the-list tr', { hasText: targetLink! })
53+
).toHaveCount(0, { timeout: 20_000 });
54+
55+
const updatedRows = page.locator('#the-list tr', { hasText: replacementUrl });
56+
if (await updatedRows.count()) {
57+
const statusCell = updatedRows.first().locator('td.column-http_status');
58+
await expect(statusCell).not.toBeEmpty();
59+
await expect(statusCell).not.toContainText(/4\d\d|5\d\d/);
60+
} else {
61+
const successNotice = page.locator('#the-list tr.no-items, #wpbody-content');
62+
await expect(successNotice).toContainText(/Action effectuée|Aucun lien cassé/i);
63+
}
64+
});
65+
});
66+
});

tests/e2e/utils/global-setup.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { chromium, FullConfig } from '@playwright/test';
2+
import fs from 'node:fs/promises';
3+
import path from 'node:path';
4+
5+
export default async function globalSetup(_config: FullConfig) {
6+
const baseURL = process.env.WP_E2E_BASE_URL;
7+
const username = process.env.WP_E2E_USERNAME;
8+
const password = process.env.WP_E2E_PASSWORD;
9+
10+
if (!baseURL || !username || !password) {
11+
console.warn(
12+
'[playwright] Skipping authenticated storage state because WP_E2E_BASE_URL, WP_E2E_USERNAME or WP_E2E_PASSWORD is not set.'
13+
);
14+
return;
15+
}
16+
17+
const storageStatePath = process.env.WP_E2E_STORAGE_STATE ?? path.resolve(__dirname, '../../../.playwright/wp-admin-state.json');
18+
await fs.mkdir(path.dirname(storageStatePath), { recursive: true });
19+
20+
const browser = await chromium.launch();
21+
const context = await browser.newContext({ baseURL });
22+
const page = await context.newPage();
23+
24+
await page.goto('/wp-login.php');
25+
await page.fill('input#user_login', username);
26+
await page.fill('input#user_pass', password);
27+
28+
await Promise.all([
29+
page.waitForURL(/\/wp-admin\//, { timeout: 60_000 }),
30+
page.click('input#wp-submit'),
31+
]);
32+
33+
await page.goto('/wp-admin/index.php');
34+
35+
await context.storageState({ path: storageStatePath });
36+
await browser.close();
37+
}

0 commit comments

Comments
 (0)