Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions e2e/browser-mode/fixtures/locator-api/rstest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from '@rstest/core';
import { BROWSER_PORTS } from '../ports';

export default defineConfig({
browser: {
enabled: true,
provider: 'playwright',
headless: true,
port: BROWSER_PORTS['locator-api'],
},
include: ['tests/**/*.test.ts'],
testTimeout: 30000,
});
129 changes: 129 additions & 0 deletions e2e/browser-mode/fixtures/locator-api/tests/locatorApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { page } from '@rstest/browser';
import { expect, test } from '@rstest/core';

function createCounter(initial = 0) {
let count = initial;

const container = document.createElement('div');
const display = document.createElement('p');
const incBtn = document.createElement('button');

display.textContent = `Count: ${count}`;
incBtn.textContent = 'Increment';
incBtn.id = 'inc-btn';

incBtn.addEventListener('click', () => {
count++;
display.textContent = `Count: ${count}`;
});

container.append(display, incBtn);
return container;
}

test('locator + expect.element proxy works', async () => {
document.body.appendChild(createCounter(5));

const nameInput = document.createElement('input');
nameInput.placeholder = 'Name';

const emailInput = document.createElement('input');
emailInput.placeholder = 'Email';

document.body.append(nameInput, emailInput);

const sections = document.createElement('div');
sections.innerHTML = `
<section>
<h2>Home</h2>
<button>Save</button>
</section>
<section>
<h2>Profile</h2>
<button>Save</button>
</section>
`;
document.body.append(sections);

await expect.element(page.getByText('Count: 5')).toBeVisible();
await page.getByRole('button', { name: 'Increment' }).click();
await expect.element(page.getByText('Count: 6')).toBeVisible();
await expect.element(page.getByText('Count: 6')).toHaveText('Count: 6');
await expect
.element(page.getByRole('button', { name: 'Increment' }))
.toHaveId('inc-btn');

await page.getByPlaceholder('Name').fill('Alice');
await expect.element(page.getByPlaceholder('Name')).toHaveValue('Alice');

await page.getByPlaceholder('Email').fill('a@b.com');
await expect.element(page.getByPlaceholder('Email')).toHaveValue('a@b.com');

// filter({ has })
const profileSave = page
.locator('section')
.filter({ has: page.getByRole('heading', { name: 'Profile' }) })
.getByRole('button', { name: 'Save' });
await expect.element(profileSave).toHaveCount(1, { timeout: 1000 });

const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'agree';

const label = document.createElement('label');
label.htmlFor = 'agree';
label.textContent = 'Agree';

document.body.append(checkbox, label);

await expect.element(page.getByLabel('Agree')).toBeUnchecked();
await page.getByLabel('Agree').check();
await expect.element(page.getByLabel('Agree')).toBeChecked();
await expect.element(page.getByLabel('Agree')).not.toBeUnchecked();
await page.getByLabel('Agree').uncheck();
await expect.element(page.getByLabel('Agree')).toBeUnchecked();
await expect.element(page.getByLabel('Agree')).not.toBeChecked();

await page.getByLabel('Agree').focus();
await expect.element(page.getByLabel('Agree')).toBeFocused();

// selectOption
const selectLabel = document.createElement('label');
selectLabel.htmlFor = 'choice';
selectLabel.textContent = 'Choice';

const select = document.createElement('select');
select.id = 'choice';
select.innerHTML = `
<option value="a">A</option>
<option value="b">B</option>
`;
document.body.append(selectLabel, select);
await page.getByLabel('Choice').selectOption('b');
await expect.element(page.getByLabel('Choice')).toHaveValue('b');

// toBeAttached/toBeDetached
const later = document.createElement('div');
later.id = 'later';
setTimeout(() => document.body.appendChild(later), 50);
await expect.element(page.locator('#later')).toBeAttached();
later.remove();
await expect.element(page.locator('#later')).toBeDetached();

// dispatchEvent
const evBtn = document.createElement('button');
evBtn.textContent = 'Event';
evBtn.addEventListener('custom', () => {
evBtn.textContent = 'Event:OK';
});
document.body.appendChild(evBtn);
await page.getByRole('button', { name: 'Event' }).dispatchEvent('custom');
await expect.element(page.getByText('Event:OK')).toBeVisible();

// and/or composition
const incBtn = page.getByRole('button', { name: 'Increment' });
await expect.element(incBtn.and(page.locator('#inc-btn'))).toHaveCount(1);
await expect
.element(page.getByPlaceholder('Name').or(page.getByPlaceholder('Email')))
.toHaveCount(2);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from '@rstest/core';

export default defineConfig({
name: 'project-a',
plugins: [pluginReact()],
include: ['tests/**/*.test.tsx'],
browser: {
enabled: true,
provider: 'playwright',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { expect, test } from '@rstest/core';

test('jsx runtime works without React in scope', () => {
const element = <span>hello</span>;
expect(element.props.children).toBe('hello');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "ESNext",
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"moduleResolution": "bundler",
"jsx": "react-jsx",
"types": ["@rstest/core"]
},
"include": ["tests", "rstest.config.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
name: 'project-b',
include: ['tests/**/*.test.ts'],
browser: {
enabled: true,
provider: 'playwright',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { expect, test } from '@rstest/core';

test('smoke', () => {
expect(1 + 1).toBe(2);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "ESNext",
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"resolveJsonModule": true,
"moduleResolution": "bundler",
"types": ["@rstest/core"]
},
"include": ["tests", "rstest.config.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
projects: ['./project-b/rstest.config.ts', './project-a/rstest.config.ts'],
});
15 changes: 15 additions & 0 deletions e2e/browser-mode/fixtures/ports.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/**
* Keep every fixture port unique.
*
* Browser-mode e2e tests can run concurrently in a single process. Reusing a port
* across fixtures can cause flaky "EADDRINUSE" failures.
*/
export const BROWSER_PORTS = {
basic: 5180,
'browser-react': 5202,
'locator-api': 5226,
list: 5204,
'no-tests': 5206,
'entry-override': 5208,
Expand All @@ -22,3 +29,11 @@ export const BROWSER_PORTS = {
'reporter-watch': 5222,
'github-actions': 5224,
} as const;

const browserPortValues = Object.values(BROWSER_PORTS);

if (new Set(browserPortValues).size !== browserPortValues.length) {
throw new Error(
`Duplicate browser fixture ports detected: ${JSON.stringify(BROWSER_PORTS)}`,
);
}
21 changes: 21 additions & 0 deletions e2e/browser-mode/locatorApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from '@rstest/core';
import { runBrowserCli } from './utils';

describe('browser mode - locator api', () => {
it('should run locator API tests correctly', async () => {
const { expectExecSuccess, cli } = await runBrowserCli('locator-api');

await expectExecSuccess();
expect(cli.stdout).toMatch(/Tests.*passed/);
});

it('should run locator API tests in headed mode without scheduler page', async () => {
const { expectExecSuccess, cli } = await runBrowserCli('locator-api', {
args: ['--browser.headless', 'false'],
});

await expectExecSuccess();
expect(cli.stdout).toMatch(/Tests.*passed/);
expect(cli.stdout).not.toContain('/scheduler.html');
});
});
21 changes: 21 additions & 0 deletions e2e/browser-mode/multiProjectConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from '@rstest/core';
import { runBrowserCli } from './utils';

/**
* Regression test: in multi-project browser mode, each project must compile with
* its own config instead of inheriting config from the first browser project.
*/
describe('browser mode - multi project config isolation', () => {
it('should apply each browser project config independently', async () => {
const { expectExecSuccess, cli } = await runBrowserCli(
'multi-project-config',
{
args: ['project-a/tests/jsxRuntime.test.tsx'],
},
);

await expectExecSuccess();
expect(cli.stdout).toContain('jsxRuntime.test.tsx');
expect(cli.stdout).toMatch(/Tests.*passed/);
});
});
10 changes: 10 additions & 0 deletions e2e/browser-mode/viewport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,14 @@ describe('browser mode - viewport', () => {
await expectExecSuccess();
expect(cli.stdout).toMatch(/Tests.*passed/);
});

it('should apply viewport config in headed mode without scheduler page', async () => {
const { expectExecSuccess, cli } = await runBrowserCli('viewport', {
args: ['--browser.headless', 'false'],
});

await expectExecSuccess();
expect(cli.stdout).toMatch(/Tests.*passed/);
expect(cli.stdout).not.toContain('/scheduler.html');
});
});
4 changes: 3 additions & 1 deletion e2e/rstest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export default defineConfig({
setupFiles: ['../scripts/rstest.setup.ts'],
// Increased timeout for CI to handle slower environments (e.g., Node.js 22 on Windows)
// and reduce flaky timeouts caused by resource contention under high parallelism.
testTimeout: process.env.CI ? 60_000 : 10_000,
// Use the same timeout locally because browser-mode e2e tests can exceed 15s when
// running together with the full suite under high machine load.
testTimeout: process.env.CI ? 60_000 : 30_000,
slowTestThreshold: 2_000,
// Stabilize date/time based e2e fixtures across different runner timezones.
// Some fixtures use `new Date('YYYY-MM-DD')` (UTC parsing) but assert on local
Expand Down
2 changes: 0 additions & 2 deletions examples/browser-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
"@rstest/browser": "workspace:*",
"@rstest/browser-react": "workspace:*",
"@rstest/core": "workspace:*",
"@testing-library/dom": "^10.4.1",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"playwright": "^1.58.2",
Expand Down
Loading
Loading