playwright config are update to date

This commit is contained in:
Nate Kelley 2025-05-01 15:31:05 -06:00
parent 0675ec2fa7
commit 9e29ffbb59
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
10 changed files with 276 additions and 44 deletions

View File

@ -15,7 +15,9 @@
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:codegen": "playwright codegen"
"test:e2e:codegen": "playwright codegen",
"test:e2e:clear-auth": "node -e \"require('fs').existsSync('./playwright-tests/auth.json') && require('fs').unlinkSync('./playwright-tests/auth.json') && console.log('Auth state cleared') || console.log('No auth state to clear')\"",
"test:e2e:setup-auth": "npx ts-node playwright-tests/auth-setup.ts"
},
"engines": {
"node": ">=22.9.0"

View File

@ -0,0 +1,149 @@
import { Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import isEmpty from 'lodash/isEmpty';
import { jwtDecode } from 'jwt-decode';
// Path to the authentication state file
export const authFile = path.join(__dirname, 'auth.json');
/**
* Checks if valid authentication data exists
*/
export function hasValidAuth(): boolean {
try {
if (!fs.existsSync(authFile)) {
return false;
}
const authData = JSON.parse(fs.readFileSync(authFile, 'utf-8'));
if (
isEmpty(authData) ||
isEmpty(authData.cookies) ||
isEmpty(authData.localStorage) ||
isEmpty(authData.sessionStorage)
) {
return false;
}
// Check if JWT is valid
if (authData.localStorage) {
const storage = JSON.parse(authData.localStorage);
const token = storage.buster_token || storage.token;
if (token) {
try {
const decoded = jwtDecode(token);
const expTime = decoded.exp ? decoded.exp * 1000 : 0; // Convert to milliseconds
if (expTime && expTime < Date.now()) {
return false;
}
} catch (error) {
return false;
}
}
}
return true;
} catch (error) {
return false;
}
}
/**
* Performs login and saves authentication state
*/
export async function login(page: Page) {
await page.goto('http://localhost:3000/auth/login');
// Add your login logic here, for example:
// await page.fill('input[name="email"]', process.env.TEST_USER_EMAIL || 'test@example.com');
// await page.fill('input[name="password"]', process.env.TEST_USER_PASSWORD || 'password123');
// await page.click('button[type="submit"]');
await page.getByText('Sign in').click();
await page.getByRole('textbox', { name: 'What is your email address?' }).fill('chad@buster.so');
await page.getByRole('textbox', { name: 'What is your email address?' }).press('Tab');
await page.getByRole('textbox', { name: 'Password' }).fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('http://localhost:3000/app/home');
// Save authentication data
const authData = {
cookies: await page.context().cookies(),
localStorage: await page.evaluate(() => JSON.stringify(localStorage)),
sessionStorage: await page.evaluate(() => JSON.stringify(sessionStorage))
};
try {
fs.writeFileSync(authFile, JSON.stringify(authData));
return authData;
} catch (error) {
console.error('Failed to save authentication data:', error);
return authData;
}
}
/**
* Applies saved authentication state to a page
*/
export async function applyAuth(page: Page): Promise<boolean> {
if (!hasValidAuth()) {
return false;
}
try {
const authData = JSON.parse(fs.readFileSync(authFile, 'utf-8'));
// Add cookies
await page.context().addCookies(authData.cookies || []);
// Set localStorage and sessionStorage
if (!isEmpty(authData.localStorage) || !isEmpty(authData.sessionStorage)) {
await page.goto('http://localhost:3000');
if (authData.localStorage) {
await page.evaluate((storageData) => {
const storage = JSON.parse(storageData);
for (const [key, value] of Object.entries(storage)) {
localStorage.setItem(key, value as string);
}
}, authData.localStorage);
}
if (authData.sessionStorage) {
await page.evaluate((storageData) => {
const storage = JSON.parse(storageData);
for (const [key, value] of Object.entries(storage)) {
sessionStorage.setItem(key, value as string);
}
}, authData.sessionStorage);
}
} else {
return false;
}
return true;
} catch (error) {
console.error('Failed to apply authentication:', error);
return false;
}
}
/**
* Clears saved authentication data
*/
export function clearAuth() {
try {
if (fs.existsSync(authFile)) {
fs.unlinkSync(authFile);
return true;
}
return false;
} catch (error) {
console.error('Failed to clear authentication data:', error);
return false;
}
}

View File

@ -0,0 +1 @@
{"cookies":[{"name":"sb-127-auth-token","value":"base64-eyJhY2Nlc3NfdG9rZW4iOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKcGMzTWlPaUpvZEhSd09pOHZNVEkzTGpBdU1DNHhPalUwTXpJeEwyRjFkR2d2ZGpFaUxDSnpkV0lpT2lKak1tUmtOalJqWkMxbU4yWXpMVFE0T0RRdFltTTVNUzFrTkRaaFpUUXpNVGt3TVdVaUxDSmhkV1FpT2lKaGRYUm9aVzUwYVdOaGRHVmtJaXdpWlhod0lqb3hOelEyTVRNM09UWTFMQ0pwWVhRaU9qRTNORFl4TXpRek5qVXNJbVZ0WVdsc0lqb2lZMmhoWkVCaWRYTjBaWEl1YzI4aUxDSndhRzl1WlNJNklpSXNJbUZ3Y0Y5dFpYUmhaR0YwWVNJNmV5SndjbTkyYVdSbGNpSTZJbVZ0WVdsc0lpd2ljSEp2ZG1sa1pYSnpJanBiSW1WdFlXbHNJbDE5TENKMWMyVnlYMjFsZEdGa1lYUmhJanA3ZlN3aWNtOXNaU0k2SW1GMWRHaGxiblJwWTJGMFpXUWlMQ0poWVd3aU9pSmhZV3d4SWl3aVlXMXlJanBiZXlKdFpYUm9iMlFpT2lKd1lYTnpkMjl5WkNJc0luUnBiV1Z6ZEdGdGNDSTZNVGMwTmpFek5ETTJOWDFkTENKelpYTnphVzl1WDJsa0lqb2lORGhoTURnMlpHSXROVGswT0MwME5tSmpMV0V6TlRFdE5qbGtaakkxT0RVd1l6bGpJaXdpYVhOZllXNXZibmx0YjNWeklqcG1ZV3h6WlgwLjUwdW1fUWdxSlA2bXRJSjlRdHJDUzFYcUhDa09uUGp4OXdrOGdzbFVCR0kiLCJ0b2tlbl90eXBlIjoiYmVhcmVyIiwiZXhwaXJlc19pbiI6MzYwMCwiZXhwaXJlc19hdCI6MTc0NjEzNzk2NSwicmVmcmVzaF90b2tlbiI6ImlqNzZ6ZGtpdGtlciIsInVzZXIiOnsiaWQiOiJjMmRkNjRjZC1mN2YzLTQ4ODQtYmM5MS1kNDZhZTQzMTkwMWUiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJlbWFpbCI6ImNoYWRAYnVzdGVyLnNvIiwiZW1haWxfY29uZmlybWVkX2F0IjoiMjAyNS0wMy0wNFQxODo0MjowNS44MDE2OTdaIiwicGhvbmUiOiIiLCJjb25maXJtZWRfYXQiOiIyMDI1LTAzLTA0VDE4OjQyOjA1LjgwMTY5N1oiLCJsYXN0X3NpZ25faW5fYXQiOiIyMDI1LTA1LTAxVDIxOjE5OjI1LjMzNDgyMDI2WiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwiaWRlbnRpdGllcyI6W3siaWRlbnRpdHlfaWQiOiJjMmRkNjRjZC1mN2YzLTQ4ODQtYmM5MS1kNDZhZTQzMTkwMWUiLCJpZCI6ImMyZGQ2NGNkLWY3ZjMtNDg4NC1iYzkxLWQ0NmFlNDMxOTAxZSIsInVzZXJfaWQiOiJjMmRkNjRjZC1mN2YzLTQ4ODQtYmM5MS1kNDZhZTQzMTkwMWUiLCJpZGVudGl0eV9kYXRhIjp7InN1YiI6ImMyZGQ2NGNkLWY3ZjMtNDg4NC1iYzkxLWQ0NmFlNDMxOTAxZSJ9LCJwcm92aWRlciI6ImVtYWlsIiwibGFzdF9zaWduX2luX2F0IjoiMjAyNS0wMy0wNFQxODo0MjowNS44MTQyNVoiLCJjcmVhdGVkX2F0IjoiMjAyNS0wMy0wNFQxODo0MjowNS44MTQyNVoiLCJ1cGRhdGVkX2F0IjoiMjAyNS0wMy0wNFQxODo0MjowNS44MTQyNVoifV0sImNyZWF0ZWRfYXQiOiIyMDI1LTAzLTA0VDE4OjQyOjA1LjgwMTY5N1oiLCJ1cGRhdGVkX2F0IjoiMjAyNS0wNS0wMVQyMToxOToyNS4zMzc5OTVaIiwiaXNfYW5vbnltb3VzIjpmYWxzZX19","domain":"localhost","path":"/","expires":1746739172.173069,"httpOnly":true,"secure":false,"sameSite":"Lax"}],"localStorage":"{}","sessionStorage":"{}"}

View File

@ -0,0 +1,37 @@
import { BusterRoutes, createBusterRoute } from '@/routes';
import { test, expect } from '@playwright/test';
const homePage = createBusterRoute({
route: BusterRoutes.APP_HOME
});
const loginPage = createBusterRoute({
route: BusterRoutes.AUTH_LOGIN
});
test.describe('Authentication Flow', () => {
test('should redirect when cookies are cleared', async ({ page, context }) => {
// First visit home page
await page.goto(homePage);
await expect(page).toHaveURL(homePage);
// Clear cookies to remove authentication
await context.clearCookies();
// Try to access the protected home page again
await page.goto(homePage);
await page.waitForTimeout(100);
// Should be redirected away from the protected route
await expect(page).not.toHaveURL(homePage);
await expect(page).toHaveURL(loginPage);
});
test('go to home page', async ({ page }) => {
await page.goto(homePage);
//for 100 milliseconds
await page.waitForTimeout(100);
await expect(page).toHaveURL(homePage);
});
});

View File

@ -0,0 +1,50 @@
import { chromium, FullConfig } from '@playwright/test';
import { applyAuth, login, authFile, hasValidAuth } from './auth-utils';
import * as fs from 'fs';
async function globalSetup(config: FullConfig) {
// Make sure auth file exists with at least empty valid JSON to prevent errors
if (!fs.existsSync(authFile)) {
try {
fs.writeFileSync(
authFile,
JSON.stringify({
cookies: [],
localStorage: '{}',
sessionStorage: '{}'
})
);
} catch (error) {
console.error('Failed to create initial auth file:', error);
// Continue - we'll handle login below
}
}
// Use chromium browser for the setup
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
console.log('global setup page');
// Check if we have valid stored credentials
if (hasValidAuth()) {
const authSuccess = await applyAuth(page);
if (authSuccess) {
// Verify login was successful by visiting a protected page
await page.goto('http://localhost:3000/app/home');
// If we're still on the login page, we need to login again
if (page.url().includes('/auth/login')) {
await login(page);
}
} else {
await login(page);
}
} else {
await login(page);
}
await browser.close();
}
export default globalSetup;

View File

@ -0,0 +1,9 @@
import { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
// Add any cleanup operations here if needed
// For example, you might want to perform some API calls to reset test data
console.log('Global teardown completed');
}
export default globalTeardown;

View File

@ -1,18 +0,0 @@
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

View File

@ -0,0 +1,13 @@
import { test, expect } from '@playwright/test';
test('Login to buster', async ({ page }) => {
//await page.getByText('Sign in').click();
await page.goto('http://localhost:3000/auth/login');
await page.getByText('Sign in').click();
await page.getByRole('textbox', { name: 'What is your email address?' }).fill('chad@buster.so');
await page.getByRole('textbox', { name: 'What is your email address?' }).press('Tab');
await page.getByRole('textbox', { name: 'Password' }).fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.goto('http://localhost:3000/app/home');
expect(page).toHaveURL('http://localhost:3000/app/home');
});

View File

@ -1,23 +0,0 @@
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('http://localhost:3000/auth/login');
await expect(page.getByRole('button', { name: 'Sign up with Google' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Sign up with Github' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Sign up with Azure' })).toBeVisible();
await expect(page.locator('body')).toContainText('Sign in');
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Sign up for free" [level=1]
- button "Sign up with Google":
- img
- button "Sign up with Github":
- img
- button "Sign up with Azure":
- img
- textbox "What is your email address?"
- textbox "Password"
- textbox "Confirm password"
- button "Sign up" [disabled]
- text: Already have an account? Sign in
`);
});

View File

@ -1,4 +1,6 @@
import { defineConfig, devices } from '@playwright/test';
import path from 'path';
import fs from 'fs';
/**
* Read environment variables from file.
@ -31,9 +33,19 @@ export default defineConfig({
/* Capture screenshot on failure */
screenshot: 'on',
/* Run tests in headed mode (non-headless) */
headless: false
headless: false,
/* Use stored auth state only if it exists */
storageState: fs.existsSync(path.join(__dirname, 'playwright-tests/auth-utils/auth.json'))
? path.join(__dirname, 'playwright-tests/auth-utils/auth.json')
: undefined
},
/* Global setup to run before all tests */
globalSetup: './playwright-tests/auth-utils/global-setup.ts',
/* Global teardown to run after all tests */
globalTeardown: './playwright-tests/auth-utils/global-teardown.ts',
/* Configure projects for major browsers */
projects: [
{
@ -77,6 +89,6 @@ export default defineConfig({
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000 // 120 seconds
timeout: 30 * 1000 // 30 seconds
}
});