-
Notifications
You must be signed in to change notification settings - Fork 2
Add Playwright e2e testing infrastructure #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { defineConfig, devices } from '@playwright/test'; | ||
|
|
||
| export default defineConfig({ | ||
| testDir: './tests', | ||
| fullyParallel: false, // Run sequentially to avoid rate limiting on Dash Platform | ||
| forbidOnly: !!process.env.CI, | ||
| retries: process.env.CI ? 2 : 1, | ||
| workers: 1, // Single worker for Dash Platform stability | ||
| reporter: [ | ||
| ['html', { outputFolder: 'playwright-report' }], | ||
| ['list'], | ||
| ], | ||
| timeout: 120000, // 2 minutes per test - Dash Platform can be slow | ||
| expect: { | ||
| timeout: 30000, // 30 seconds for assertions | ||
| }, | ||
| use: { | ||
| baseURL: 'http://localhost:3000', | ||
| trace: 'retain-on-failure', | ||
| screenshot: 'only-on-failure', | ||
| video: 'retain-on-failure', | ||
| actionTimeout: 30000, | ||
| navigationTimeout: 60000, | ||
| }, | ||
| projects: [ | ||
| { | ||
| name: 'chromium', | ||
| use: { ...devices['Desktop Chrome'] }, | ||
| }, | ||
| ], | ||
| webServer: { | ||
| command: 'npm run dev', | ||
| url: 'http://localhost:3000', | ||
| reuseExistingServer: true, // Always reuse existing server | ||
| timeout: 120000, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| import { test, expect } from '../../fixtures/test-fixtures'; | ||
| import { LoginPage } from '../../pages/login.page'; | ||
| import { FeedPage } from '../../pages/feed.page'; | ||
| import { TEST_IDENTITY, getHighAuthKey } from '../../fixtures/test-identity'; | ||
|
|
||
| test.describe('Authentication - Login', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| // Clear any stored credentials | ||
| await page.goto('/'); | ||
| await page.evaluate(() => { | ||
| localStorage.clear(); | ||
| sessionStorage.clear(); | ||
| }); | ||
| }); | ||
|
|
||
| test('should display login page correctly', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Verify page elements are visible | ||
| await expect(page.getByText('Yappr')).toBeVisible(); | ||
| await expect(page.getByText('Sign in with your Dash Platform identity')).toBeVisible(); | ||
| await expect(loginPage.identityInput).toBeVisible(); | ||
| await expect(loginPage.credentialInput).toBeVisible(); | ||
| await expect(loginPage.signInButton).toBeVisible(); | ||
| await expect(loginPage.faucetLink).toBeVisible(); | ||
| await expect(loginPage.bridgeLink).toBeVisible(); | ||
| }); | ||
|
|
||
| test('should validate identity ID format', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Enter valid identity ID | ||
| await loginPage.enterIdentity(TEST_IDENTITY.identityId); | ||
|
|
||
| // Wait for validation | ||
| await loginPage.waitForIdentityValidation(); | ||
|
|
||
| // Should show valid indicator | ||
| const isResolved = await loginPage.isIdentityResolved(); | ||
| expect(isResolved).toBe(true); | ||
| }); | ||
|
|
||
| test('should show error for invalid identity', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Enter invalid identity | ||
| await loginPage.enterIdentity('invalid-identity-id-that-doesnt-exist'); | ||
|
|
||
| // Wait for validation | ||
| await page.waitForTimeout(2000); | ||
|
|
||
| // Should show error message | ||
| const errorVisible = await loginPage.lookupError.isVisible().catch(() => false); | ||
| expect(errorVisible).toBe(true); | ||
| }); | ||
|
|
||
| test('should validate private key against identity', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Enter identity | ||
| await loginPage.enterIdentity(TEST_IDENTITY.identityId); | ||
| await loginPage.waitForIdentityValidation(); | ||
|
|
||
| // Enter correct private key | ||
| await loginPage.enterCredential(getHighAuthKey()); | ||
| await loginPage.waitForKeyValidation(); | ||
|
|
||
| // Sign in button should be enabled | ||
| const isEnabled = await loginPage.isLoginEnabled(); | ||
| expect(isEnabled).toBe(true); | ||
| }); | ||
|
|
||
| test('should reject incorrect private key', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Enter identity | ||
| await loginPage.enterIdentity(TEST_IDENTITY.identityId); | ||
| await loginPage.waitForIdentityValidation(); | ||
|
|
||
| // Enter incorrect private key (valid WIF but wrong identity) | ||
| await loginPage.enterCredential('cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy'); | ||
| await loginPage.waitForKeyValidation(); | ||
|
|
||
| // Sign in button should be disabled | ||
| const isEnabled = await loginPage.isLoginEnabled(); | ||
| expect(isEnabled).toBe(false); | ||
| }); | ||
|
|
||
| test('should successfully login with valid credentials', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Perform login | ||
| await loginPage.login(TEST_IDENTITY.identityId, getHighAuthKey()); | ||
|
|
||
| // Wait for redirect to feed | ||
| await page.waitForURL(/\/feed/, { timeout: 30000 }); | ||
|
|
||
| // Verify we're on the feed page | ||
| const feedPage = new FeedPage(page); | ||
| await expect(feedPage.homeTitle).toBeVisible({ timeout: 10000 }); | ||
| }); | ||
|
|
||
| test('should toggle remember me setting', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Check initial state | ||
| const initialState = await loginPage.rememberMeToggle.getAttribute('aria-checked'); | ||
|
|
||
| // Toggle | ||
| await loginPage.toggleRememberMe(); | ||
|
|
||
| // Verify state changed | ||
| const newState = await loginPage.rememberMeToggle.getAttribute('aria-checked'); | ||
| expect(newState).not.toBe(initialState); | ||
| }); | ||
|
|
||
| test('should toggle credential visibility', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Enter some credential | ||
| await loginPage.enterIdentity(TEST_IDENTITY.identityId); | ||
| await loginPage.waitForIdentityValidation(); | ||
| await loginPage.credentialInput.fill('test-credential'); | ||
|
|
||
| // Check initial type is password | ||
| const initialType = await loginPage.credentialInput.getAttribute('type'); | ||
| expect(initialType).toBe('password'); | ||
|
|
||
| // Click show toggle | ||
| await page.locator('button').filter({ has: page.locator('svg') }).nth(1).click(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking: Credential visibility test targets a nonexistent second SVG button After identity validation, the login form has only one button containing an SVG: the credential eye toggle. The remember-me switch contains a span and the idle submit button contains text, so selecting source: ['codex'] |
||
|
|
||
| // Verify type changed to text | ||
| const newType = await loginPage.credentialInput.getAttribute('type'); | ||
| expect(newType).toBe('text'); | ||
| }); | ||
|
|
||
| test('should redirect authenticated users from homepage to feed', async ({ page }) => { | ||
| const loginPage = new LoginPage(page); | ||
| await loginPage.goto(); | ||
|
|
||
| // Login | ||
| await loginPage.login(TEST_IDENTITY.identityId, getHighAuthKey()); | ||
| await page.waitForURL(/\/feed/, { timeout: 30000 }); | ||
|
|
||
| // Navigate to homepage | ||
| await page.goto('/'); | ||
|
|
||
| // Should redirect to feed | ||
| await page.waitForURL(/\/feed/, { timeout: 10000 }); | ||
| }); | ||
| }); | ||
|
|
||
| test.describe('Authentication - Logout', () => { | ||
| test('should successfully logout', async ({ authenticatedPage }) => { | ||
| const page = authenticatedPage; | ||
|
|
||
| // Navigate to settings | ||
| await page.goto('/settings'); | ||
| await page.waitForLoadState('networkidle'); | ||
|
|
||
| // Click logout | ||
| const logoutButton = page.getByRole('button', { name: /logout|sign out/i }); | ||
| if (await logoutButton.isVisible()) { | ||
| await logoutButton.click(); | ||
|
|
||
| // Should redirect to login or home | ||
| await page.waitForURL(/\/login|\/$/, { timeout: 10000 }); | ||
| } | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Blocking: Clean installations do not provision the configured Chromium browser
The new scripts run Playwright directly, but the PR adds no installation script, CI step, or documentation invoking
playwright install; the locked Playwright 1.57 packages also have no browser-download install script. Thereforenpm installinstalls the runner but not its Chromium executable, sonpm testfails at browser launch on a clean machine or CI runner without a preexisting Playwright cache.source: ['codex']