Skip to content
Open
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
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
"build": "next build",
"build:gh-pages": "GITHUB_PAGES=true next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed",
"test:debug": "playwright test --debug",
"test:report": "playwright show-report"
Comment on lines +11 to +15

Copy link
Copy Markdown
Collaborator

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. Therefore npm install installs the runner but not its Chromium executable, so npm test fails at browser launch on a clean machine or CI runner without a preexisting Playwright cache.

source: ['codex']

},
"dependencies": {
"@dashevo/evo-sdk": "^3.0.0-dev.9",
Expand Down Expand Up @@ -40,6 +45,7 @@
"zustand": "^4.5.0"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
37 changes: 37 additions & 0 deletions playwright.config.ts
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,
},
});
178 changes: 178 additions & 0 deletions tests/e2e/auth/login.spec.ts
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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 .nth(1) never resolves and the click times out. The existing showCredentialToggle page-object locator already identifies the correct first button.

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 });
}
});
});
Loading