@drupal/playwright

中文文档
2.0.0发布于 2026/11/06GPL-2.0+TypeScriptES Module近 30 天下载 1.4万

用 Playwright 测试 Drupal 站点的固定和命令

中文使用文档
文档目录9 节
  1. 所需资源
  2. 固定设备
  3. 完全平行
  4. 默认
  5. 自定义站点安装
  6. 每项试验隔离
  7. 每个测试被隔离,有快照
  8. 每个文件被隔离
  9. 子目录中的站点

剧作家

此工程提供定着和帮助对象用于写入 剧作家 测试与 Drupal 网站交互.

所需资源

  • 节点.js 22.16.0左右.

固定设备

为安装Drupal并随后工作提供了两个固定装置 带着它 用于处理 drupal 对象的文档是 这里。 。 。 .

必须指出的是,默认测试可以运行在任何可用的 工人,这意味着它可能运行在 Drupal 安装之前的一套 测试已经开始 这可以结合测试来改变 配置和您使用的固定.

完全平行

都摆好了 fullyParallel: true 内 playwright.config.ts,或 test.describe.configure({ mode: 'parallel' }); 在您的光谱文件中.

在描述区块内平行运行的测试。 这样可以更快地执行 因为测试不等待对方, 但要求测试是独立的 没有共享状态,因为任何测试 都可能最终在任何工人 任何顺序。 这意味着 使用这个固定装置进行的测试 决不能对它造成任何破坏 网站。 但是,只要清理完之后,它们就可以进行改建 如果测试失败, Playwright 将丢弃整个工人 程序并开始新的程序)。 这也适用于任何在钩子上运行的东西 例如 beforeAll - 它可能最终运行 在多个工人,这是 变化保持不变.

flowchart TD
 subgraph W1["Worker 1"]
  W1si["Site install"]
  W1g1["group1 beforeAll"]
  W1g1t1["group1 test1"]
  W1g2["group2 beforeAll"]
  W1g2t2["group2 test3 - FAIL, end worker
  flaky test example
  (retries: 1)"]
 end
 subgraph W2["Worker 2"]
  W2si["Site install"]
  W2g1["group1 beforeAll"]
  W2g1t2["group1 test2"]
  W2g2["group2 beforeAll"]
  W2g2t2["group2 test2"]
  W2g3["group3 beforeAll"]
  W2g3t2["group3 test2"]
  W2g4["group4 beforeAll"]
  W2g4t1["group4 test1"]
  W2g42["group4 beforeAll"]
  W2g4t2["group4 test2"]
  W2g4t3["group4 test3"]
 end
 subgraph W3["Worker 3"]
  W3si["Site install"]
  W3g1["group1 beforeAll"]
  W3g1t3["group1 test3 - FAIL, end worker
  (retries: 0)"]
 end
 subgraph W4["Worker 4"]
  W4si["Site install"]
  W4g2["group2 beforeAll"]
  W4g2t1["group2 test1"]
  W4g3["group3 beforeAll"]
  W4g3t1["group3 test1"]
  W4g32["group3 beforeAll"]
  W4g3t3["group3 test3"]
 end
 subgraph W5["Worker 5"]
    W5si["Site install"]
    W5g4["group4 beforeAll"]
    W5g4t3["group4 test3"]
 end
 subgraph W6["Worker 6"]
  W6si["Site install"]
  W6g2["group2 beforeAll"]
  W6g2t3["group2 test3"]
 end

W1si --> W1g1
W1g1 --> W1g1t1
W1g1t1 --> W1g2
W1g2 --> W1g2t2

 W2si --> W2g1
 W2g1 --> W2g1t2
 W2g1t2 --> W2g2
 W2g2 --> W2g2t2
 W2g2t2 --> W2g3
 W2g3 --> W2g3t2
 W2g3t2 --> W2g4
 W2g4 --> W2g4t1
 W2g4t1 --> W2g42
 W2g42 --> W2g4t2
 W2g4t2 --> W2g4t3

W3si --> W3g1
W3g1 --> W3g1t3

W4si --> W4g2
W4g2 --> W4g2t1
W4g2t1 --> W4g3
W4g3 --> W4g3t1
W4g3t1 --> W4g32
W4g32 --> W4g3t3

W5si --> W5g4
W5g4 --> W5g4t3

W6si --> W6g2
W6g2 --> W6g2t3

W3 --> W5
W3g1t3 -.-> W5si
W1 --> W6
W1g2t2 -.-> W6si

 A["fullyParallel: true"] --> W1si
 A --> W2si
 A --> W3si
 A --> W4si
import { expect } from '@playwright/test';
import { parallelWorker as test } from '@drupal/playwright';

test.describe('Drupal', () => {
  test('Can create a role, add permissions, and create a user', async ({
    drupal,
    page,
  }) => {
    await drupal.loginAsAdmin();
    await drupal.createRole({ name: 'content editor' });
    await drupal.addPermissions({
      role: 'content editor',
      permissions: ['access content overview', 'administer blocks'],
    });
    await drupal.createUser({
      username: 'catbro',
      password: 'meow',
      email: '[email protected]',
      roles: ['content editor'],
    });
    await drupal.logout();
    await drupal.login({ username: 'catbro', password: 'meow' });
    await page.goto('./admin/content');
    await expect(
      page.getByText('There are no content items yet.'),
    ).toBeVisible();
    await page.goto('./admin/structure/block');
    await expect(page.locator('h1')).toHaveText('Block layout');
    await expect(
      page.locator('table[data-drupal-selector="edit-blocks"]'),
    ).toBeVisible();
  });
});

默认

都摆好了 fullyParallel: false 内 playwright.config.ts,或 test.describe.configure({ mode: 'default' }); 在你的光谱文件中, 但你会 仍然导入 parallelWorker 修补.

在描述区块内进行测试,顺序为同一工人工序 测试平行运行的文件 。 然而,测试仍可能分布在 工人和工人仍然可能被重新使用.

flowchart TD
 subgraph W1["Worker 1"]
  W1si["Site install"]
  W1g1["group1 beforeAll"]
  W1g1t1["group1 test1"]
  W1g1t2["group1 test2"]
  W1g1t3["group1 test3"]
  W1g3["group3 beforeAll"]
  W1g3t1["group3 test1"]
  W1g3t2["group3 test2 - FAIL, end worker
  (retries: 0)"]
 end
 subgraph W2["Worker 2"]
  W2si["Site install"]
  W2g2["group2 beforeAll"]
  W2g2t1["group2 test1"]
  W2g2t2["group2 test2"]
  W2g2t3["group2 test3"]
  W2g4["group4 beforeAll"]
  W2g4t1["group4 test1"]
  W2g4t2["group4 test2"]
  W2g4t3["group4 test3"]
 end
 subgraph W3["Worker 3"]
  W3si["Site install"]
  W3g3["group3 beforeAll"]
  W3g3t3["group3 test3"]
 end

W1 --> W3

W1si --> W1g1
W1g1 --> W1g1t1
W1g1t1 --> W1g1t2
W1g1t2 --> W1g1t3
W1g1t3 --> W1g3
W1g3 --> W1g3t1
W1g3t1 --> W1g3t2

W2si --> W2g2
W2g2 --> W2g2t1
W2g2t1 --> W2g2t2
W2g2t2 --> W2g2t3
W2g2t3 --> W2g4
W2g4 --> W2g4t1
W2g4t1 --> W2g4t2
W2g4t2 --> W2g4t3

W3si --> W3g3
W3g3 --> W3g3t3

W1g3t2 -.-> W3si

 A["fullyParallel: false"] --> W1si
 A --> W2si

自定义站点安装

这个 parallelWorker 站点安装配置有工人范围选项. 因为它是工人范围, 设置在文件的顶端,在 playwright.config.ts,或者在再出口的预配置上 test (不在内) 个人 test()) (中文(简体) ). 宣布不同值的测试在不同的工人中进行, 每一个都安装了自己的网站.

Playwright保证这一点:工人只被重新用于测试,而其试验是: 工作范围选项匹配它开始的选项, 所以文件使用 standard 绝对不会对工人的 minimal 网站。 如果不然的话 安排在这样的工人, Playwright 停止该工人并开始一个新的, 用于安装所请求配置文件的网站。 声明相同的文件 配置文件仍然可以共享一个工人。 设置 installProfile 内置 a test.describe() 块是一个加载时间错误,因为它需要一个新的 工人通过文件.

因此,运行中使用的每个不同的剖面图至少需要一个额外的站点 安装,所以在可能情况下,倾向于每个套件的单个配置.

选项默认说明
installProfile'minimal'安装传递给 Drupal.install()。 。 。 .
import { parallelWorker as test } from '@drupal/playwright';

test.use({ installProfile: 'standard' });

每项试验隔离

每个测试可以使用一个孤立的 Drupal 安装 isolatedPerTest 修补.

flowchart TD
 subgraph W1["Worker 1"]
  W1g1["group1 beforeAll"]
  W1si1["Site install"]
  W1g1t1["group1 test1"]
  W1g3["group3 beforeAll"]
  W1si2["Site install"]
  W1g3t1["group3 test1"]
  W1si3["Site install"]
  W1g3t2["group3 test2"]
  W1si4["Site install"]
  W1g3t3["group3 test3"]
 end
 subgraph W2["Worker 2"]
  W2g1["group2 beforeAll"]
  W2si1["Site install"]
  W2g2t1["group2 test1"]
  W2si2["Site install"]
  W2g2t2["group2 test2"]
  W2si3["Site install"]
  W2g2t3["group2 test3"]
  W2g4["group4 beforeAll"]
  W2si4["Site install"]
  W2g4t1["group4 test1"]
  W2si5["Site install"]
  W2g4t2["group4 test2"]
  W2si6["Site install"]
  W2g4t3["group4 test3"]
 end

W1g1 --> W1si1
W1si1 --> W1g1t1
W1g1t1 --> W1g3
W1g3 --> W1si2
W1si2 --> W1g3t1
W1g3t1 --> W1si3
W1si3 --> W1g3t2
W1g3t2 --> W1si4
W1si4 --> W1g3t3

W2g1 --> W2si1
W2si1 --> W2g2t1
W2g2t1 --> W2si2
W2si2 --> W2g2t2
W2g2t2 --> W2si3
W2si3 --> W2g2t3
W2g2t3 --> W2g4
W2g4 --> W2si4
W2si4 --> W2g4t1
W2g4t1 --> W2si5
W2si5 --> W2g4t2
W2g4t2 --> W2si6
W2si6 --> W2g4t3

 A["isolatedPerTest"] --> W1g1
 A --> W2g1
import { expect } from '@playwright/test';
import { isolatedPerTest as test } from '@drupal/playwright';

test.describe('Drupal', () => {
  test('Install a module', async ({ drupal, page }) => {
    await drupal.loginAsAdmin();
    const response = await page.goto('./admin/structure/views');
    expect(response).not.toBeNull();
    expect(response!.status()).toBe(404);
    await drupal.installModules(['views', 'views_ui']);
    await page.goto('./admin/structure/views');
    await expect(page.locator('h1')).toHaveText('Views');
  });

  test('Verify views is not installed', async ({ drupal, page }) => {
    await drupal.loginAsAdmin();
    const response = await page.goto('./admin/structure/views');
    expect(response).not.toBeNull();
    expect(response!.status()).toBe(404);
  });
});

整个文件的选项可以通过 test.use数字 :

import { expect } from '@playwright/test';
import { isolatedPerTest as test } from '@drupal/playwright';

test.use({ installProfile: 'standard', modules: ['views', 'views_ui'], enableTestExtensions: true });

test.describe('Drupal', () => {
    test('Do something', async ({ drupal, page }) => {

每个测试被隔离,有快照

这个 isolatedPerTestSnapshot fixture 使每次测试都相同 孤立为 isolatedPerTest,但安装网站 每个工人一次 财务报告和审定财务报表 在每次测试前恢复快照,而不是重排。 这对 安装了大量剖面图或食谱的站点,使测试运行得更快 仍然保证测试之间不泄露数据.

需要sqlite(英语:Sqlite). 复制独立 sqlite 的快照/ 备份作品 数据库(和所管理的文件目录)。 设定 DRUPAL_TEST_DB_URL 改为 sqlite:// URL (中文(简体) ). 与其它数据库一起,固定器立即投出——使用 isolatedPerTest 或者说 parallelWorker 换句话说.

import { expect } from '@playwright/test';
import { isolatedPerTestSnapshot as test } from '@drupal/playwright';

test.describe('Drupal', () => {
  test('Install a module', async ({ drupal, page }) => {
    await drupal.loginAsAdmin();
    await drupal.installModules(['views', 'views_ui']);
    await page.goto('./admin/structure/views');
    await expect(page.locator('h1')).toHaveText('Views');
  });

  test('Verify views is not installed', async ({ drupal, page }) => {
    // The previous test's module install was rolled back by the restore.
    await drupal.loginAsAdmin();
    const response = await page.goto('./admin/structure/views');
    expect(response!.status()).toBe(404);
  });
});

什么恢复覆盖

在每次测试前,固定恢复数据库和网站 files 财务报告和审定财务报表 private 目录. 其他站点文件, 例如 settings.php,没有 恢复。 数据库与 sqlite 复制 在线备份 API,所以这是安全的,即使 前一个测试的请求仍在运行中 .

恢复后不需要缓存清除 。 恢复数据库缓存表格 和数据库一起拍摄的快照没有 last_write_timestamp_* 缓存项,所以 Drupal 忽略了上一个项 测试在APCu.

自定义快照

被烤入快照的布局配置有工人范围选项. 因为他们是工人范围, 让他们在文件的顶端,在 playwright.config.ts,或者在再出口的预配置上 test (不在内) 个人 test()) (中文(简体) ). 宣布不同值的测试在不同的工人中进行, 每一个都有自己的快照.

选项默认说明
installProfile'minimal'安装传递给 Drupal.install()。 。 。 .
modules[]在快照前公开安装模块 .
enableTestExtensionsfalse使德鲁帕尔的只测试模块可以发现.
snapshotCachefalse重新使用一个工人的快照 横穿,而代码不变.
test.use({ installProfile: 'standard', modules: ['views', 'views_ui'] });

建议的消费项目模式是再出口预先配置的 test 因此单个规格不需要锅炉板:

// tests/fixtures/test.ts
import { isolatedPerTestSnapshot } from '@drupal/playwright';

export const test = isolatedPerTestSnapshot;
test.use({
  installProfile: 'my_distribution',
  enableTestExtensions: true,
  modules: ['views', 'views_ui'],
});
// tests/my-feature.spec.ts
import { expect } from '@playwright/test';
import { test } from './fixtures/test.js';

test('starts from the pre-configured snapshot', async ({ drupal, page }) => {
  await drupal.loginAsAdmin();
  // ...
});

重用快照横行

默认每次运行时都会从零开始重建快照. 启用 snapshotCache 将每个工人的工地固定起来,然后恢复运行 它不重置重置, 跳过安装和设置 完全在一个缓存命中. 仅在编辑测试文件时,这样做非常有用.

test.use({ snapshotCache: true });

缓存由可以改变一切的指纹键入 在快照中显示: installProfile, (中文(简体) ), modules 财务报告和审定财务报表 enableTestExtensions 选项,解析后的数据库/基址,并通过 git Drupal 状态 根之活树. 测试文件被排除在指纹之外 所以 编辑a *.spec.ts 重用缓存快照,同时更改模块, 配置或其他源文件重建它. 仅 *.spec.ts 被排除为 默认, 设置 testFileGlobs 以匹配不同的布局。 只有当前 指纹保存在磁盘上; 较旧的快照自动被刷出 .

需要 git( 或 a) key) (中文(简体) ). 工作树的指纹取决于 德鲁帕尔根是一个git检查出。 如果不是,你也不会提供 key, (中文(简体) ), 缓存以警告方式禁用自己, 而不是冒着重用 stale 的风险 示意图.

通过一个对象来调谐它 :

test.use({
  snapshotCache: {
    // Provide a custom cache key
    key: process.env.MY_CACHE_KEY,
    // Where snapshots are stored
    // defaults to a directory under the OS temp
    // Also configurable with DRUPAL_TEST_SNAPSHOT_CACHE_DIR
    dir: '.cache/playwright-snapshots',
    // Paths excluded from the git fingerprint (defaults to ['**/*.spec.ts'])
    testFileGlobs: ['**/*.spec.ts', 'tests/**'],
  },
});

夹克也可以强迫上或下 DRUPAL_TEST_SNAPSHOT_CACHE 环境变量,它取代了选项 – 方便保持它 本地但关闭 CI :

DRUPAL_TEST_SNAPSHOT_CACHE=0 npx playwright test   # force off (e.g. in CI)
DRUPAL_TEST_SNAPSHOT_CACHE=1 npx playwright test   # force on

每个工人只重用自己写的插槽 所以平行运行停留 孤立,恢复的数据库保留它安装的前缀。 一个跑步 使用更多的工人 比缓存运行 只是重建额外的工人 快照和缓存它们.

每个文件被隔离

您也可以每个测试文件安装一个孤立的 Drupal, 但这需要 一些手工作业。 不过,它能确保测试数据不泄露 在测试文件之外,并使用 default 模式.

如果你想把一组测试放在一个文件中, 你可以结合 这些战略旨在同时对共享工人进行一些测试 安装 和一些隔离.

flowchart TD
 subgraph W1["Worker 1"]
  W1g1["group1 beforeAll (Site install)"]
  W1g1t1["group1 test1"]
  W1g1t2["group1 test2"]
  W1g1t3["group1 test3"]
  W1g3["group3 beforeAll (Site install)"]
  W1g3t1["group3 test1"]
  W1g3t2["group3 test2"]
  W1g3t3["group3 test3"]
 end
 subgraph W2["Worker 2"]
  W2g2["group2 beforeAll (Site install)"]
  W2g2t1["group2 test1"]
  W2g2t2["group2 test2"]
  W2g2t3["group2 test3"]
  W2g4["group4 beforeAll (Site install)"]
  W2g4t1["group4 test1"]
  W2g4t2["group4 test2"]
  W2g4t3["group4 test3"]
 end

 W1g1 --> W1g1t1
 W1g1t1 --> W1g1t2
 W1g1t2 --> W1g1t3
 W1g1t3 --> W1g3
 W1g3 --> W1g3t1
 W1g3t1 --> W1g3t2
 W1g3t2 --> W1g3t3

 W2g2 --> W2g2t1
 W2g2t1 --> W2g2t2
 W2g2t2 --> W2g2t3
 W2g2t3 --> W2g4
 W2g4 --> W2g4t1
 W2g4t1 --> W2g4t2
 W2g4t2 --> W2g4t3


 A["isolatedPerTest"] --> W1g1
 A --> W2g2
import { Drupal, type DrupalSite } from '@drupal/playwright';
import { test, expect } from '@playwright/test';

test.describe('Isolated', () => {
  let drupalSite: DrupalSite;
  let drupal: Drupal;

  test.beforeAll(async ({ browser }) => {
    drupalSite = await Drupal.install();
    const page = await browser.newPage();
    drupal = new Drupal({ page, drupalSite });
    await drupal.setTestCookie();
    await drupal.loginAsAdmin();
    await drupal.setPreprocessing({ css: true, javascript: true });
    await drupal.logout();
    await page.close();
  });
  test.beforeEach(async ({ page }) => {
    drupal = new Drupal({ page, drupalSite });
    await drupal.setTestCookie();
  });
  test.afterAll(async ({}) => {
    await drupalSite.teardown();
  });

  test('Verify CSS and JS aggregation are on', async ({ page }) => {
    await drupal.loginAsAdmin();
    await page.goto('./admin/config/development/performance');
    await expect(page.getByLabel('Aggregate CSS files')).toBeChecked();
    await expect(page.getByLabel('Aggregate JavaScript files')).toBeChecked();
  });
});
import { Drupal, type DrupalSite } from '@drupal/playwright';
import { test, expect } from '@playwright/test';

test.describe.configure({ mode: 'default' });

test.describe('Isolated (Serial)', () => {
  let drupalSite: DrupalSite;
  let drupal: Drupal;

  test.beforeAll(async ({ browser }) => {
    drupalSite = await Drupal.install();
  });
  test.beforeEach(async ({ page }) => {
    drupal = new Drupal({ page, drupalSite });
    await drupal.setTestCookie();
  });
  test.afterAll(async ({}) => {
    await drupalSite.teardown();
  });

  test('Turn CSS and JS aggregation on', async ({ page }) => {
    await drupal.loginAsAdmin();
    await drupal.setPreprocessing({ css: true, javascript: true });
  });

  test('Verify CSS and JS aggregation are on', async ({ page }) => {
    await drupal.loginAsAdmin();
    await page.goto('./admin/config/development/performance');
    await expect(page.getByLabel('Aggregate CSS files')).toBeChecked();
    await expect(page.getByLabel('Aggregate JavaScript files')).toBeChecked();
  });
});

子目录中的站点

如果您的测试可能针对安装在子目录中的 Drupal 站点, 例如 . http://localhost/web/ 在 Drupal.org CI 上,导航路径 baseURL,从 ./数字 :

await page.goto('./user/login'); // ✓
await page.goto('/user/login'); // ✗ ignores any path in baseURL

领先 / 将子目录去掉,所以测试404s. 这也适用于: waitForURL(), (中文(简体) ), toHaveURL() 财务报告和审定财务报表 page.request。固定装置添加后导 斜线 baseURL 需要做到这一点。 如果你的网站总是从 root,任意一种形式都可行.

抓住根相通的路径 用 eslint --fix,添加 ESLint 语句 插件为 eslint.config.mjs数字 :

import drupalPlaywright from '@drupal/playwright/eslint';

export default [
  // ...
  drupalPlaywright.configs.recommended,
];

版本与发布记录

@drupal/playwright 共发布 8 个版本,首次发布于 2025/08/27;过去 12 个月发布了 5 个版本。

版本发布日期距今
2.0.0latest2026/11/067 天前
1.3.02026/10/3014 天前
1.2.02026/07/024 个月前
1.1.22026/06/184 个月前
1.1.12026/03/257 个月前
1.1.02025/09/251.1 年前

关于 @drupal/playwright 的常见问题

@drupal/playwright 是什么?

用 Playwright 测试 Drupal 站点的固定和命令

如何安装 @drupal/playwright?

在项目目录运行 npm install @drupal/playwright,或使用 pnpm add / yarn add / bun add。

@drupal/playwright 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@drupal/playwright 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@drupal/playwright 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.16.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@drupal/playwright 可以商用吗?

@drupal/playwright 使用 GPL-2.0+ 许可。强 Copyleft:分发包含该代码的软件时,整个作品需以兼容的 GPL 许可开源。仅在内部使用、不对外分发时不触发此义务。以上为概括,不构成法律意见,请以许可原文为准。

@drupal/playwright 的下载量有多少?

根据 npm 官方下载统计,@drupal/playwright 在 2026/10/13 – 2026/11/11 这 30 天内被下载 14,387 次,日均约 480 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@drupal/playwright 还在维护吗?

最新版本 2.0.0 发布于 2026/11/06(7 天前);至今共发布 8 个版本,其中过去 12 个月发布了 5 个;首次发布于 2025/08/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。