@wp-playground/mcp 3.1.5 → 3.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,679 +0,0 @@
1
- import { test as base, expect } from '@playwright/test';
2
- import type { Page } from '@playwright/test';
3
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
5
- import { dirname } from 'path';
6
- import { fileURLToPath } from 'url';
7
- import WebSocket from 'ws';
8
-
9
- // Use a random port so tests are isolated from real browser tabs
10
- // that might also connect to the default MCP WebSocket port.
11
- const MCP_WS_PORT = 17999 + Math.floor(Math.random() * 1000);
12
-
13
- type McpTestFixtures = {
14
- siteId: string;
15
- };
16
-
17
- type McpWorkerFixtures = {
18
- mcpClient: Client;
19
- playgroundPage: Page;
20
- };
21
-
22
- const test = base.extend<McpTestFixtures, McpWorkerFixtures>({
23
- mcpClient: [
24
- // eslint-disable-next-line no-empty-pattern
25
- async ({}, use) => {
26
- const transport = new StdioClientTransport({
27
- command: 'node',
28
- args: [
29
- '--experimental-strip-types',
30
- '--experimental-transform-types',
31
- '--import',
32
- '../../../meta/src/node-es-module-loader/register.mts',
33
- '../src/index.ts',
34
- `--port=${MCP_WS_PORT}`,
35
- ],
36
- cwd: dirname(fileURLToPath(import.meta.url)),
37
- env: {
38
- ...process.env,
39
- NODE_NO_WARNINGS: '1',
40
- } as Record<string, string>,
41
- });
42
- const client = new Client({
43
- name: 'playwright-mcp-test',
44
- version: '1.0.0',
45
- });
46
- await client.connect(transport);
47
- await use(client);
48
- await client.close();
49
- },
50
- { scope: 'worker' },
51
- ],
52
-
53
- // Auto-fixture: loads the Playground website in a real browser.
54
- // The MCP bridge auto-connects via WebSocket and registers sites
55
- // from the Redux store. The bridge reconnects every 5s if dropped.
56
- playgroundPage: [
57
- async ({ browser, mcpClient }, use) => {
58
- const page = await browser.newPage();
59
- await page.goto(
60
- `http://127.0.0.1:5400/website-server/?mcp=yes&mcp-port=${MCP_WS_PORT}`
61
- );
62
-
63
- // Wait for WordPress to load inside the nested iframes
64
- await expect(
65
- page
66
- .frameLocator(
67
- '#playground-viewport:visible,.playground-viewport:visible'
68
- )
69
- .frameLocator('#wp')
70
- .locator('body')
71
- ).not.toBeEmpty();
72
-
73
- // Wait for the MCP bridge to register at least one active
74
- // site. The Playground website may do internal navigation
75
- // after the initial load, causing the bridge to disconnect
76
- // and reconnect. We wait long enough for the connection to
77
- // stabilize.
78
- await waitForActiveSite(mcpClient);
79
-
80
- await use(page);
81
- await page.close();
82
- },
83
- { scope: 'worker', auto: true },
84
- ],
85
-
86
- siteId: async ({ mcpClient }, use) => {
87
- const siteId = await waitForActiveSite(mcpClient, 30_000);
88
- await use(siteId);
89
- },
90
- });
91
-
92
- function resultText(result: Awaited<ReturnType<Client['callTool']>>): string {
93
- return (result.content as Array<{ text: string }>)[0].text;
94
- }
95
-
96
- /**
97
- * Poll playground_list_sites until at least one active site is found
98
- * AND verify the site can actually handle commands. The MCP bridge
99
- * reconnects every 5s, so this may need to wait through a
100
- * disconnect/reconnect cycle. After finding an active site, we
101
- * verify it's operational by calling getCurrentURL — the
102
- * PlaygroundClient in the browser may not be ready immediately
103
- * after the site is registered.
104
- */
105
- async function waitForActiveSite(
106
- client: Client,
107
- timeoutMs = 60_000,
108
- { probe: shouldProbe = true } = {}
109
- ): Promise<string> {
110
- const start = Date.now();
111
- let lastError: Error | undefined;
112
- while (Date.now() - start < timeoutMs) {
113
- try {
114
- const result = await client.callTool({
115
- name: 'playground_list_sites',
116
- arguments: {},
117
- });
118
- const parsed = JSON.parse(resultText(result));
119
- if (parsed.connectedTabs === 0) {
120
- throw new Error('No browser connected yet');
121
- }
122
- const activeSite = parsed.sites.find((s) => s.isActive);
123
- if (!activeSite) {
124
- throw new Error('No active site yet');
125
- }
126
- const siteId = activeSite.siteId;
127
- if (shouldProbe) {
128
- // Verify the site can actually handle commands.
129
- // The PlaygroundClient may not be ready immediately
130
- // after the bridge registers the site.
131
- const probeResult = await client.callTool({
132
- name: 'playground_get_site_info',
133
- arguments: { siteId },
134
- });
135
- if (probeResult.isError) {
136
- throw new Error('Site not ready for commands yet');
137
- }
138
- }
139
- return siteId;
140
- } catch (error) {
141
- lastError = error as Error;
142
- await new Promise((r) => setTimeout(r, 2_000));
143
- }
144
- }
145
- throw lastError ?? new Error('Timeout waiting for active site');
146
- }
147
-
148
- test.afterEach(async ({ mcpClient, playgroundPage, browser }) => {
149
- let needsReset = false;
150
-
151
- for (const context of browser.contexts()) {
152
- for (const page of context.pages()) {
153
- if (page !== playgroundPage) {
154
- await page.close();
155
- needsReset = true;
156
- }
157
- }
158
- }
159
-
160
- if (!playgroundPage.url().includes('website-server')) {
161
- needsReset = true;
162
- }
163
-
164
- if (needsReset) {
165
- await playgroundPage.goto(
166
- `http://127.0.0.1:5400/website-server/?mcp=yes&mcp-port=${MCP_WS_PORT}`
167
- );
168
- await waitForActiveSite(mcpClient, 60_000, { probe: false });
169
- }
170
- });
171
-
172
- test('lists all 16 registered tools', async ({ mcpClient }) => {
173
- const result = await mcpClient.listTools();
174
- expect(result.tools).toHaveLength(16);
175
- const names = result.tools.map((t) => t.name).sort();
176
- expect(names).toEqual([
177
- 'playground_delete_directory',
178
- 'playground_delete_file',
179
- 'playground_execute_php',
180
- 'playground_file_exists',
181
- 'playground_get_current_url',
182
- 'playground_get_site_info',
183
- 'playground_list_files',
184
- 'playground_list_sites',
185
- 'playground_mkdir',
186
- 'playground_navigate',
187
- 'playground_open_site',
188
- 'playground_read_file',
189
- 'playground_rename_site',
190
- 'playground_request',
191
- 'playground_save_site',
192
- 'playground_write_file',
193
- ]);
194
- });
195
-
196
- test('playground_list_sites includes playground url with mcp params', async ({
197
- mcpClient,
198
- siteId,
199
- }) => {
200
- const result = await mcpClient.callTool({
201
- name: 'playground_list_sites',
202
- arguments: {},
203
- });
204
- const parsed = JSON.parse(resultText(result));
205
- const site = parsed.sites.find(
206
- (s: { siteId: string }) => s.siteId === siteId
207
- );
208
- expect(site).toBeDefined();
209
- expect(site.url).toMatch(new RegExp(`\\?mcp=yes&mcp-port=${MCP_WS_PORT}$`));
210
- });
211
-
212
- test('playground_open_site activates an inactive site in a new tab', async ({
213
- mcpClient,
214
- playgroundPage,
215
- siteId,
216
- }) => {
217
- // Save the site so it persists in OPFS across page reloads
218
- await mcpClient.callTool({
219
- name: 'playground_save_site',
220
- arguments: { siteId },
221
- });
222
-
223
- // Reload the Playground without ?site-slug. This creates a
224
- // new temporary site (active) while loading the saved site
225
- // from OPFS (inactive).
226
- await playgroundPage.goto(
227
- `http://127.0.0.1:5400/website-server/?mcp=yes&mcp-port=${MCP_WS_PORT}`
228
- );
229
- await expect(
230
- playgroundPage
231
- .frameLocator(
232
- '#playground-viewport:visible,.playground-viewport:visible'
233
- )
234
- .frameLocator('#wp')
235
- .locator('body')
236
- ).not.toBeEmpty();
237
-
238
- // Wait for the saved site to appear as inactive
239
- await expect
240
- .poll(
241
- async () => {
242
- const result = await mcpClient.callTool({
243
- name: 'playground_list_sites',
244
- arguments: {},
245
- });
246
- const parsed = JSON.parse(resultText(result));
247
- const site = parsed.sites.find((s) => s.siteId === siteId);
248
- return site?.isActive;
249
- },
250
- { timeout: 30_000, intervals: [2_000] }
251
- )
252
- .toBe(false);
253
-
254
- // Open the inactive site — the browser calls window.open(),
255
- // a new tab loads, and the site becomes active.
256
- await mcpClient.callTool({
257
- name: 'playground_open_site',
258
- arguments: { siteId },
259
- });
260
-
261
- // Verify list_sites now reports the site as active
262
- await expect
263
- .poll(
264
- async () => {
265
- const result = await mcpClient.callTool({
266
- name: 'playground_list_sites',
267
- arguments: {},
268
- });
269
- const parsed = JSON.parse(resultText(result));
270
- const site = parsed.sites.find((s) => s.siteId === siteId);
271
- return site?.isActive;
272
- },
273
- { timeout: 30_000, intervals: [2_000] }
274
- )
275
- .toBe(true);
276
- });
277
-
278
- test('playground_list_sites returns at least one site', async ({
279
- mcpClient,
280
- siteId,
281
- }) => {
282
- const result = await mcpClient.callTool({
283
- name: 'playground_list_sites',
284
- arguments: {},
285
- });
286
- const parsed = JSON.parse(resultText(result));
287
- expect(parsed.connectedTabs).toBeGreaterThan(0);
288
- expect(parsed.sites.length).toBeGreaterThan(0);
289
- expect(parsed.sites.find((s) => s.siteId === siteId)).toBeTruthy();
290
- });
291
-
292
- test('playground_navigate goes to /wp-admin/', async ({
293
- mcpClient,
294
- siteId,
295
- }) => {
296
- const result = await mcpClient.callTool({
297
- name: 'playground_navigate',
298
- arguments: { siteId, path: '/wp-admin/' },
299
- });
300
- const parsed = JSON.parse(resultText(result));
301
- expect(parsed.url).toContain('wp-admin');
302
- });
303
-
304
- test('playground_execute_php runs code and returns output', async ({
305
- mcpClient,
306
- siteId,
307
- }) => {
308
- const result = await mcpClient.callTool({
309
- name: 'playground_execute_php',
310
- arguments: {
311
- siteId,
312
- code: '<?php echo "Hello from PHP " . phpversion();',
313
- },
314
- });
315
- const parsed = JSON.parse(resultText(result));
316
- expect(parsed.text).toContain('Hello from PHP');
317
- expect(parsed.exitCode).toBe(0);
318
- });
319
-
320
- test('playground_request fetches the homepage', async ({
321
- mcpClient,
322
- siteId,
323
- }) => {
324
- const result = await mcpClient.callTool({
325
- name: 'playground_request',
326
- arguments: { siteId, url: '/wp-admin/' },
327
- });
328
- const parsed = JSON.parse(resultText(result));
329
- expect(parsed.httpStatusCode).toBe(200);
330
- expect(parsed.text).toContain('Dashboard');
331
- });
332
-
333
- test('playground_write_file, playground_read_file, and playground_delete_file', async ({
334
- mcpClient,
335
- siteId,
336
- }) => {
337
- const testPath = '/wordpress/wp-content/e2e-test.txt';
338
- const testContent = `E2E test at ${Date.now()}`;
339
-
340
- const writeResult = await mcpClient.callTool({
341
- name: 'playground_write_file',
342
- arguments: { siteId, path: testPath, contents: testContent },
343
- });
344
- expect(JSON.parse(resultText(writeResult)).success).toBe(true);
345
-
346
- const readResult = await mcpClient.callTool({
347
- name: 'playground_read_file',
348
- arguments: { siteId, path: testPath },
349
- });
350
- expect(JSON.parse(resultText(readResult)).contents).toBe(testContent);
351
-
352
- await mcpClient.callTool({
353
- name: 'playground_delete_file',
354
- arguments: { siteId, path: testPath },
355
- });
356
-
357
- const readAfterDelete = await mcpClient.callTool({
358
- name: 'playground_read_file',
359
- arguments: { siteId, path: testPath },
360
- });
361
- expect(resultText(readAfterDelete)).toContain('Error');
362
- });
363
-
364
- test('playground_list_files lists the plugins directory', async ({
365
- mcpClient,
366
- siteId,
367
- }) => {
368
- const result = await mcpClient.callTool({
369
- name: 'playground_list_files',
370
- arguments: { siteId, path: '/wordpress/' },
371
- });
372
- const parsed = JSON.parse(resultText(result));
373
- expect(parsed.files).toBeInstanceOf(Array);
374
- const files = parsed.files as string[];
375
- expect(files.length).toBeGreaterThan(0);
376
- expect(parsed.files).toContain('wp-content');
377
- expect(parsed.files).toContain('wp-load.php');
378
- });
379
-
380
- test('playground_mkdir creates and verifies a directory and playground_delete_directory removes it', async ({
381
- mcpClient,
382
- siteId,
383
- }) => {
384
- const testDir = '/wordpress/wp-content/e2e-test-dir';
385
-
386
- const mkdirResult = await mcpClient.callTool({
387
- name: 'playground_mkdir',
388
- arguments: { siteId, path: testDir },
389
- });
390
- expect(JSON.parse(resultText(mkdirResult)).success).toBe(true);
391
-
392
- const listResult = await mcpClient.callTool({
393
- name: 'playground_list_files',
394
- arguments: { siteId, path: '/wordpress/wp-content' },
395
- });
396
- const files = JSON.parse(resultText(listResult)).files as string[];
397
- expect(files).toContain('e2e-test-dir');
398
-
399
- await mcpClient.callTool({
400
- name: 'playground_delete_directory',
401
- arguments: { siteId, path: testDir },
402
- });
403
-
404
- const listAfterDelete = await mcpClient.callTool({
405
- name: 'playground_list_files',
406
- arguments: { siteId, path: '/wordpress/wp-content' },
407
- });
408
- const filesAfterDelete = JSON.parse(resultText(listAfterDelete))
409
- .files as string[];
410
- expect(filesAfterDelete).not.toContain('e2e-test-dir');
411
- });
412
-
413
- test('playground_delete_directory with recursive=true removes a non-empty directory', async ({
414
- mcpClient,
415
- siteId,
416
- }) => {
417
- const testDir = '/wordpress/wp-content/e2e-recursive-dir';
418
- const nestedFile = `${testDir}/subdir/nested.txt`;
419
-
420
- // Create a nested structure: e2e-recursive-dir/subdir/nested.txt
421
- await mcpClient.callTool({
422
- name: 'playground_mkdir',
423
- arguments: { siteId, path: `${testDir}/subdir` },
424
- });
425
- await mcpClient.callTool({
426
- name: 'playground_write_file',
427
- arguments: { siteId, path: nestedFile, contents: 'nested content' },
428
- });
429
-
430
- // Verify the file exists
431
- const readResult = await mcpClient.callTool({
432
- name: 'playground_read_file',
433
- arguments: { siteId, path: nestedFile },
434
- });
435
- expect(JSON.parse(resultText(readResult)).contents).toBe('nested content');
436
-
437
- // Recursive delete should remove the entire tree
438
- const deleteResult = await mcpClient.callTool({
439
- name: 'playground_delete_directory',
440
- arguments: { siteId, path: testDir, recursive: true },
441
- });
442
- expect(JSON.parse(resultText(deleteResult)).success).toBe(true);
443
-
444
- // Verify the directory is gone
445
- const listAfterDelete = await mcpClient.callTool({
446
- name: 'playground_list_files',
447
- arguments: { siteId, path: '/wordpress/wp-content' },
448
- });
449
- const filesAfterDelete = JSON.parse(resultText(listAfterDelete))
450
- .files as string[];
451
- expect(filesAfterDelete).not.toContain('e2e-recursive-dir');
452
- });
453
-
454
- test('playground_get_site_info returns WordPress details', async ({
455
- mcpClient,
456
- siteId,
457
- }) => {
458
- const result = await mcpClient.callTool({
459
- name: 'playground_get_site_info',
460
- arguments: { siteId },
461
- });
462
- const parsed = JSON.parse(resultText(result));
463
- expect(parsed.wpVersion).toBeTruthy();
464
- expect(parsed.wpVersion).not.toBe('unknown');
465
- expect(parsed.phpVersion).toBeTruthy();
466
- expect(parsed.phpVersion).not.toBe('unknown');
467
- expect(parsed.documentRoot).toMatch('/wordpress');
468
- expect(parsed.siteUrl).toMatch(new RegExp(`http(.)+`));
469
- });
470
-
471
- test('playground_rename_site renames an active site', async ({
472
- mcpClient,
473
- siteId,
474
- }) => {
475
- // Get the original name so we can restore it
476
- const listBefore = await mcpClient.callTool({
477
- name: 'playground_list_sites',
478
- arguments: {},
479
- });
480
- const originalName = JSON.parse(resultText(listBefore)).sites.find(
481
- (s) => s.siteId === siteId
482
- )?.name;
483
-
484
- const result = await mcpClient.callTool({
485
- name: 'playground_rename_site',
486
- arguments: { siteId, newName: 'E2E Renamed Site' },
487
- });
488
- expect(result.isError).toBeFalsy();
489
- const parsed = JSON.parse(resultText(result));
490
- expect(parsed.success).toBe(true);
491
- expect(parsed.newName).toBe('E2E Renamed Site');
492
-
493
- // Verify the name changed in list_sites
494
- const listAfter = await mcpClient.callTool({
495
- name: 'playground_list_sites',
496
- arguments: {},
497
- });
498
- const renamedSite = JSON.parse(resultText(listAfter)).sites.find(
499
- (s) => s.siteId === siteId
500
- );
501
- expect(renamedSite?.name).toBe('E2E Renamed Site');
502
-
503
- // Restore the original name
504
- if (originalName) {
505
- await mcpClient.callTool({
506
- name: 'playground_rename_site',
507
- arguments: { siteId, newName: originalName },
508
- });
509
- }
510
- });
511
-
512
- test('playground_save_site persists a temporary site', async ({
513
- mcpClient,
514
- siteId,
515
- }) => {
516
- const result = await mcpClient.callTool({
517
- name: 'playground_save_site',
518
- arguments: { siteId },
519
- });
520
- expect(result.isError).toBeFalsy();
521
- const parsed = JSON.parse(resultText(result));
522
- expect(parsed.success).toBe(true);
523
-
524
- // Verify the site is now stored in opfs
525
- const listResult = await mcpClient.callTool({
526
- name: 'playground_list_sites',
527
- arguments: {},
528
- });
529
- const savedSite = JSON.parse(resultText(listResult)).sites.find(
530
- (s) => s.siteId === siteId
531
- );
532
- expect(savedSite?.storage).toBe('opfs');
533
- });
534
-
535
- test('playground_get_current_url returns a path', async ({
536
- mcpClient,
537
- siteId,
538
- }) => {
539
- await mcpClient.callTool({
540
- name: 'playground_navigate',
541
- arguments: { siteId, path: '/wp-admin/' },
542
- });
543
- const result = await mcpClient.callTool({
544
- name: 'playground_get_current_url',
545
- arguments: { siteId },
546
- });
547
- const parsed = JSON.parse(resultText(result));
548
- expect(parsed.url).toBe('/wp-admin/');
549
- });
550
-
551
- test('playground_file_exists checks for wp-config.php', async ({
552
- mcpClient,
553
- siteId,
554
- }) => {
555
- const result = await mcpClient.callTool({
556
- name: 'playground_file_exists',
557
- arguments: { siteId, path: '/wordpress/wp-config.php' },
558
- });
559
- const parsed = JSON.parse(resultText(result));
560
- expect(parsed.exists).toBe(true);
561
-
562
- const missing = await mcpClient.callTool({
563
- name: 'playground_file_exists',
564
- arguments: { siteId, path: '/wordpress/does-not-exist.txt' },
565
- });
566
- const missingParsed = JSON.parse(resultText(missing));
567
- expect(missingParsed.exists).toBe(false);
568
- });
569
-
570
- test('playground_list_sites reports no browser when page navigates away', async ({
571
- mcpClient,
572
- playgroundPage,
573
- }) => {
574
- await playgroundPage.goto('about:blank');
575
- await expect
576
- .poll(
577
- async () => {
578
- const result = await mcpClient.callTool({
579
- name: 'playground_list_sites',
580
- arguments: {},
581
- });
582
- const parsed = JSON.parse(resultText(result));
583
- return parsed.connectedTabs;
584
- },
585
- { timeout: 15_000, intervals: [1_000] }
586
- )
587
- .toBe(0);
588
- });
589
-
590
- test('playground_list_sites reports connected but no sites when browser has no playground tab', async ({
591
- mcpClient,
592
- playgroundPage,
593
- browser,
594
- }) => {
595
- // Disconnect the real Playground bridge
596
- await playgroundPage.goto('about:blank');
597
- await expect
598
- .poll(
599
- async () => {
600
- const result = await mcpClient.callTool({
601
- name: 'playground_list_sites',
602
- arguments: {},
603
- });
604
- return JSON.parse(resultText(result)).connectedTabs;
605
- },
606
- { timeout: 15_000, intervals: [1_000] }
607
- )
608
- .toBe(0);
609
-
610
- // Open a bare page and connect a WebSocket that registers
611
- // with zero sites — simulating a browser tab that has no
612
- // Playground loaded.
613
- const wsPort = MCP_WS_PORT;
614
- const fakePage = await browser.newPage();
615
- // Navigate to an allowed origin so the WebSocket connection
616
- // passes the origin check in the bridge server.
617
- await fakePage.goto(`http://127.0.0.1:5400`);
618
- await fakePage.evaluate(async (port) => {
619
- // Fetch the session token before connecting
620
- const res = await fetch(`http://127.0.0.1:${port}/bridge-token`);
621
- const { token } = await res.json();
622
-
623
- return new Promise<void>((resolve, reject) => {
624
- const ws = new WebSocket(`ws://127.0.0.1:${port}?token=${token}`);
625
- ws.addEventListener('open', () => {
626
- ws.send(
627
- JSON.stringify({
628
- type: 'register',
629
- tabId: 'test-empty-tab',
630
- sites: [],
631
- })
632
- );
633
- resolve();
634
- });
635
- ws.addEventListener('error', () =>
636
- reject(new Error('WebSocket failed'))
637
- );
638
- });
639
- }, wsPort);
640
-
641
- await expect
642
- .poll(
643
- async () => {
644
- const result = await mcpClient.callTool({
645
- name: 'playground_list_sites',
646
- arguments: {},
647
- });
648
- const parsed = JSON.parse(resultText(result));
649
- return {
650
- connectedTabs: parsed.connectedTabs,
651
- siteCount: parsed.sites.length,
652
- };
653
- },
654
- { timeout: 15_000, intervals: [1_000] }
655
- )
656
- .toEqual({ connectedTabs: 1, siteCount: 0 });
657
- });
658
-
659
- test('rejects WebSocket connections without a valid token', async ({
660
- mcpClient,
661
- }) => {
662
- // Verify the bridge is running
663
- const tools = await mcpClient.listTools();
664
- expect(tools.tools.length).toBeGreaterThan(0);
665
-
666
- // Try connecting without a token — should be rejected.
667
- // The server rejects during the HTTP upgrade with 401,
668
- // which the ws library surfaces as an 'unexpected-response'
669
- // event (or an error + close).
670
- const ws = new WebSocket(`ws://127.0.0.1:${MCP_WS_PORT}`);
671
- const rejected = new Promise<void>((resolve) => {
672
- ws.on('unexpected-response', (_req, res) => {
673
- expect(res.statusCode).toBe(401);
674
- resolve();
675
- });
676
- });
677
-
678
- await rejected;
679
- });
@@ -1,35 +0,0 @@
1
- import { defineConfig, devices } from '@playwright/test';
2
-
3
- export default defineConfig({
4
- testDir: './e2e',
5
- fullyParallel: false,
6
- forbidOnly: !!process.env.CI,
7
- retries: process.env.CI ? 2 : 0,
8
- // Must be 1: the MCP server supports only one browser connection at a time.
9
- workers: 1,
10
- reporter: [['list', { printSteps: true }]],
11
- use: {
12
- baseURL: 'http://127.0.0.1:5400/website-server/',
13
- trace: 'on-first-retry',
14
- actionTimeout: 120_000,
15
- navigationTimeout: 120_000,
16
- },
17
- timeout: 300_000,
18
- expect: { timeout: 60_000 },
19
- projects: [
20
- {
21
- name: 'chromium',
22
- use: {
23
- ...devices['Desktop Chrome'],
24
- launchOptions: {
25
- args: ['--js-flags=--enable-experimental-webassembly-jspi'],
26
- },
27
- },
28
- },
29
- ],
30
- webServer: {
31
- command: 'npx nx run playground-website:dev',
32
- url: 'http://127.0.0.1:5400/website-server/',
33
- reuseExistingServer: !process.env.CI,
34
- },
35
- });