Feature/playwright #43

Merged
ilia merged 7 commits from feature/playwright into main 2026-05-28 13:31:17 +00:00
6 changed files with 127 additions and 4 deletions
Showing only changes of commit a9229342e6 - Show all commits
+3
View File
@@ -50,3 +50,6 @@ storybook-static
# Tests # Tests
coverage/ coverage/
.aider* .aider*
playwright-report/
blob-report/
.playwright/
+16
View File
@@ -0,0 +1,16 @@
import type { Page } from '@playwright/test';
/**
* Shared base for all page objects. Subclasses extend this and expose
* domain-specific locators + actions — never raw selectors leaking into tests.
*/
export abstract class BasePage {
protected constructor(protected readonly page: Page) {}
/**
* Navigate to a path relative to baseURL.
*/
async goto(path = '/') {
await this.page.goto(path);
}
}
+36
View File
@@ -0,0 +1,36 @@
import type {
Locator,
Page,
} from '@playwright/test';
import { BasePage } from './base-page';
/**
* Page object for the root comparison view. Encapsulates locators for the
* primary controls so tests don't hardcode aria-labels or DOM structure.
*/
export class ComparisonPage extends BasePage {
readonly searchInput: Locator;
readonly previewInput: Locator;
constructor(page: Page) {
super(page);
this.searchInput = page.getByRole('textbox', { name: 'Search typefaces' });
this.previewInput = page.getByRole('textbox', { name: 'Preview text' });
}
/**
* Open the root page and wait for the main controls to be interactable.
*/
async open() {
await this.goto('/');
await this.searchInput.waitFor({ state: 'visible' });
}
async searchFor(query: string) {
await this.searchInput.fill(query);
}
async setPreviewText(text: string) {
await this.previewInput.fill(text);
}
}
+23
View File
@@ -0,0 +1,23 @@
import {
expect,
test,
} from '@playwright/test';
import { ComparisonPage } from './pages/comparison-page';
test.describe('smoke', () => {
test('loads the comparison view with its primary controls', async ({ page }) => {
const view = new ComparisonPage(page);
await view.open();
await expect(view.searchInput).toBeVisible();
await expect(view.previewInput).toBeVisible();
});
test('accepts a search query', async ({ page }) => {
const view = new ComparisonPage(page);
await view.open();
await view.searchFor('Inter');
await expect(view.searchInput).toHaveValue('Inter');
});
});
+47 -3
View File
@@ -1,10 +1,54 @@
import { defineConfig } from '@playwright/test'; import {
defineConfig,
devices,
} from '@playwright/test';
/**
* E2E config. Tests run against the production build via `vite preview` on port 4173.
* Locally: all three browser engines run in parallel.
* CI: chromium only, workers=1 — the runner has 6GB RAM and `yarn build` already
* spikes 12GB, so we keep the E2E peak bounded.
*/
const isCI = !!process.env.CI;
export default defineConfig({ export default defineConfig({
testDir: 'e2e',
testMatch: /.*\.test\.ts$/,
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? 1 : undefined,
reporter: isCI
? [['html', { open: 'never' }], ['github']]
: [['html', { open: 'on-failure' }], ['list']],
use: {
baseURL: 'http://localhost:4173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: isCI
? [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }]
: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
webServer: { webServer: {
command: 'yarn build && yarn preview', command: 'yarn build && yarn preview',
port: 4173, port: 4173,
reuseExistingServer: true, reuseExistingServer: !isCI,
timeout: 120_000,
}, },
testDir: 'e2e',
}); });
+2 -1
View File
@@ -40,7 +40,8 @@
"src/**/*.d.ts", "src/**/*.d.ts",
"vitest.config*.ts", "vitest.config*.ts",
"vitest.setup*.ts", "vitest.setup*.ts",
"vitest.types.d.ts" "vitest.types.d.ts",
"playwright.config*.ts"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",