Fix failing e2e tests (#460)

* fix broken tests

* navbar fixes + return pricing

* changes

* re-remove pricing
This commit is contained in:
Franjo Mindek
2025-07-30 15:19:59 +02:00
committed by GitHub
parent 0a7a04e63d
commit 3b7a67d5ae
11 changed files with 64 additions and 83 deletions

View File

@@ -97,7 +97,7 @@
+ </Button> + </Button>
</WaspRouterLink> </WaspRouterLink>
) : ( ) : (
<div className='space-y-2'> <ul className='space-y-2'>
@@ -174,7 +187,14 @@ @@ -174,7 +187,14 @@
)} )}
</div> </div>
@@ -114,7 +114,7 @@
</div> </div>
</div> </div>
</div> </div>
@@ -217,6 +237,6 @@ @@ -218,6 +238,6 @@
'size-7': isScrolled, 'size-7': isScrolled,
})} })}
src={logo} src={logo}

View File

@@ -1,6 +1,6 @@
--- template/app/src/client/components/NavBar/constants.ts --- template/app/src/client/components/NavBar/constants.ts
+++ opensaas-sh/app/src/client/components/NavBar/constants.ts +++ opensaas-sh/app/src/client/components/NavBar/constants.ts
@@ -9,12 +9,12 @@ @@ -9,7 +9,6 @@
export const marketingNavigationItems: NavigationItem[] = [ export const marketingNavigationItems: NavigationItem[] = [
{ name: 'Features', to: '/#features' }, { name: 'Features', to: '/#features' },
@@ -8,9 +8,3 @@
...staticNavigationItems, ...staticNavigationItems,
] as const; ] as const;
export const demoNavigationitems: NavigationItem[] = [
{ name: 'AI Scheduler', to: routes.DemoAppRoute.to },
{ name: 'File Upload', to: routes.FileUploadRoute.to },
+ { name: 'Pricing', to: routes.PricingPageRoute.to },
...staticNavigationItems,
] as const;

View File

@@ -168,9 +168,9 @@ function NavBarMobileMenu({
</div> </div>
</WaspRouterLink> </WaspRouterLink>
) : ( ) : (
<div className='space-y-2'> <ul className='space-y-2'>
<UserMenuItems user={user} onItemClick={() => setMobileMenuOpen(false)} /> <UserMenuItems user={user} onItemClick={() => setMobileMenuOpen(false)} />
</div> </ul>
)} )}
</div> </div>
<div className='py-6'> <div className='py-6'>
@@ -202,6 +202,7 @@ function renderNavigationItems(
to={item.to} to={item.to}
className={menuStyles} className={menuStyles}
onClick={setMobileMenuOpen && (() => setMobileMenuOpen(false))} onClick={setMobileMenuOpen && (() => setMobileMenuOpen(false))}
target={item.to.startsWith('http') ? '_blank' : undefined}
> >
{item.name} {item.name}
</ReactRouterLink> </ReactRouterLink>

View File

@@ -16,5 +16,6 @@ export const marketingNavigationItems: NavigationItem[] = [
export const demoNavigationitems: NavigationItem[] = [ export const demoNavigationitems: NavigationItem[] = [
{ name: 'AI Scheduler', to: routes.DemoAppRoute.to }, { name: 'AI Scheduler', to: routes.DemoAppRoute.to },
{ name: 'File Upload', to: routes.FileUploadRoute.to }, { name: 'File Upload', to: routes.FileUploadRoute.to },
{ name: 'Pricing', to: routes.PricingPageRoute.to },
...staticNavigationItems, ...staticNavigationItems,
] as const; ] as const;

View File

@@ -211,6 +211,7 @@ function NewTaskForm({ handleCreateTask }: { handleCreateTask: typeof createTask
variant='default' variant='default'
size='default' size='default'
className='w-full' className='w-full'
data-testid='generate-schedule-button'
> >
{isPlanGenerating ? ( {isPlanGenerating ? (
<> <>
@@ -310,7 +311,7 @@ function Todo({ id, isDone, description, time }: TodoProps) {
function Schedule({ schedule }: { schedule: GeneratedSchedule }) { function Schedule({ schedule }: { schedule: GeneratedSchedule }) {
return ( return (
<div className='flex flex-col gap-6 py-6'> <div className='flex flex-col gap-6 py-6' data-testid='schedule'>
<div className='space-y-4'> <div className='space-y-4'>
{!!schedule.tasks ? ( {!!schedule.tasks ? (
schedule.tasks schedule.tasks

View File

@@ -31,13 +31,13 @@ export function UserDropdown({ user }: { user: Partial<UserEntity> }) {
if (item.isAdminOnly && (!user || !user.isAdmin)) return null; if (item.isAdminOnly && (!user || !user.isAdmin)) return null;
return ( return (
<DropdownMenuItem asChild> <DropdownMenuItem key={item.name}>
<WaspRouterLink <WaspRouterLink
to={item.to} to={item.to}
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
}} }}
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-primary' className='flex items-center gap-3 w-full'
> >
<item.icon size='1.1rem' /> <item.icon size='1.1rem' />
{item.name} {item.name}
@@ -45,12 +45,11 @@ export function UserDropdown({ user }: { user: Partial<UserEntity> }) {
</DropdownMenuItem> </DropdownMenuItem>
); );
})} })}
<DropdownMenuItem <DropdownMenuItem>
onClick={() => logout()} <button type='button' onClick={() => logout()} className='flex items-center gap-3 w-full'>
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-primary' <LogOut size='1.1rem' />
> Log Out
<LogOut size='1.1rem' /> </button>
Log Out
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@@ -12,30 +12,30 @@ export const UserMenuItems = ({ user, onItemClick }: { user?: Partial<User>; onI
if (item.isAdminOnly && (!user || !user.isAdmin)) return null; if (item.isAdminOnly && (!user || !user.isAdmin)) return null;
return ( return (
<div key={item.name} className='py-2'> <li key={item.name}>
<WaspRouterLink <WaspRouterLink
to={item.to} to={item.to}
onClick={onItemClick} onClick={onItemClick}
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out text-foreground hover:text-primary' className='flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium leading-7 text-foreground hover:bg-accent hover:text-accent-foreground transition-colors'
> >
<item.icon size='1.1rem' /> <item.icon size='1.1rem' />
{item.name} {item.name}
</WaspRouterLink> </WaspRouterLink>
</div> </li>
); );
})} })}
<div className='py-2'> <li>
<button <button
onClick={() => { onClick={() => {
logout(); logout();
onItemClick?.(); onItemClick?.();
}} }}
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out text-foreground hover:text-primary' className='flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium leading-7 text-foreground hover:bg-accent hover:text-accent-foreground transition-colors'
> >
<LogOut size='1.1rem' /> <LogOut size='1.1rem' />
Log Out Log Out
</button> </button>
</div> </li>
</> </>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { test, expect, type Page } from '@playwright/test'; import { expect, test, type Page } from '@playwright/test';
import { signUserUp, logUserIn, createRandomUser, makeStripePayment, type User} from './utils'; import { createRandomUser, logUserIn, makeStripePayment, signUserUp, type User } from './utils';
let page: Page; let page: Page;
let testUser: User; let testUser: User;
@@ -23,8 +23,8 @@ test.afterAll(async () => {
await page.close(); await page.close();
}); });
const task1 = 'create presentation on SaaS'; const task1 = 'Create presentation on SaaS';
const task2 = 'build SaaS app draft'; const task2 = 'Build SaaS app draft';
test('User can make 3 AI schedule generations', async () => { test('User can make 3 AI schedule generations', async () => {
test.slow(); // Use a longer timeout time in case OpenAI is slow to respond test.slow(); // Use a longer timeout time in case OpenAI is slow to respond
@@ -38,11 +38,13 @@ test('User can make 3 AI schedule generations', async () => {
await expect(page.getByText(task2)).toBeVisible(); await expect(page.getByText(task2)).toBeVisible();
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const generateScheduleButton = page.getByRole('button', { name: 'Generate Schedule' }); const generateScheduleButton = page.getByTestId('generate-schedule-button');
await expect(generateScheduleButton).toBeVisible(); await expect(generateScheduleButton).toBeVisible();
await Promise.all([ await Promise.all([
page.waitForRequest((req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST'), page.waitForRequest(
(req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST'
),
page.waitForResponse((response) => { page.waitForResponse((response) => {
return response.url().includes('/operations/generate-gpt-response') && response.status() === 200; return response.url().includes('/operations/generate-gpt-response') && response.status() === 200;
}), }),
@@ -50,13 +52,9 @@ test('User can make 3 AI schedule generations', async () => {
generateScheduleButton.click(), generateScheduleButton.click(),
]); ]);
// We already show a table with some dummy data even before the API call const schedule = page.getByTestId('schedule');
// Now we want to check that the tasks we added are in the generated table expect(schedule).toContainText(task1, { ignoreCase: true });
const table = page.getByRole('table'); expect(schedule).toContainText(task2, { ignoreCase: true });
await expect(table).toBeVisible();
const tableTextContent = (await table.innerText()).toLowerCase();
expect(tableTextContent.includes(task1.toLowerCase())).toBeTruthy();
expect(tableTextContent.includes(task2.toLowerCase())).toBeTruthy();
} }
}); });
@@ -65,12 +63,13 @@ test('AI schedule generation fails on 4th attempt', async () => {
await page.reload(); await page.reload();
const generateScheduleButton = page.getByRole('button', { name: 'Generate Schedule' }); const generateScheduleButton = page.getByTestId('generate-schedule-button');
await expect(generateScheduleButton).toBeVisible(); await expect(generateScheduleButton).toBeVisible();
await Promise.all([ await Promise.all([
page.waitForRequest((req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST'), page.waitForRequest(
(req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST'
),
page.waitForResponse((response) => { page.waitForResponse((response) => {
// expect the response to be 402 "PAYMENT_REQUIRED" // expect the response to be 402 "PAYMENT_REQUIRED"
return response.url().includes('/operations/generate-gpt-response') && response.status() === 402; return response.url().includes('/operations/generate-gpt-response') && response.status() === 402;
@@ -79,14 +78,9 @@ test('AI schedule generation fails on 4th attempt', async () => {
generateScheduleButton.click(), generateScheduleButton.click(),
]); ]);
// We already show a table with some dummy data even before the API call const schedule = page.getByTestId('schedule');
// Now we want to check that the tasks were NOT added because the API call should have failed expect(schedule).not.toContainText(task1, { ignoreCase: true });
const table = page.getByRole('table'); expect(schedule).not.toContainText(task2, { ignoreCase: true });
await expect(table).toBeVisible();
const tableTextContent = (await table.innerText()).toLowerCase();
expect(tableTextContent.includes(task1.toLowerCase())).toBeFalsy();
expect(tableTextContent.includes(task2.toLowerCase())).toBeFalsy();
}); });
test('Make test payment with Stripe for hobby plan', async () => { test('Make test payment with Stripe for hobby plan', async () => {
@@ -97,12 +91,14 @@ test('Make test payment with Stripe for hobby plan', async () => {
test('User should be able to generate another schedule after payment', async () => { test('User should be able to generate another schedule after payment', async () => {
await page.goto('/demo-app'); await page.goto('/demo-app');
const generateScheduleButton = page.getByRole('button', { name: 'Generate Schedule' }); const generateScheduleButton = page.getByTestId('generate-schedule-button');
await expect(generateScheduleButton).toBeVisible(); await expect(generateScheduleButton).toBeVisible();
await Promise.all([ await Promise.all([
page page
.waitForRequest((req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST') .waitForRequest(
(req) => req.url().includes('operations/generate-gpt-response') && req.method() === 'POST'
)
.catch((err) => console.error(err.message)), .catch((err) => console.error(err.message)),
page page
.waitForResponse((response) => { .waitForResponse((response) => {
@@ -116,10 +112,7 @@ test('User should be able to generate another schedule after payment', async ()
generateScheduleButton.click(), generateScheduleButton.click(),
]); ]);
await page.waitForSelector('table'); const schedule = page.getByTestId('schedule');
const table = page.getByRole('table'); expect(schedule).toContainText(task1, { ignoreCase: true });
await expect(table).toBeVisible(); expect(schedule).toContainText(task2, { ignoreCase: true });
const tableTextContent = (await table.innerText()).toLowerCase();
expect(tableTextContent.includes(task1.toLowerCase())).toBeTruthy();
expect(tableTextContent.includes(task2.toLowerCase())).toBeTruthy();
}); });

View File

@@ -1,6 +1,4 @@
import { test, expect, Cookie } from '@playwright/test'; import { Cookie, expect, test } from '@playwright/test';
const DOCS_URL = 'https://docs.opensaas.sh';
test.describe('general landing page tests', () => { test.describe('general landing page tests', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@@ -13,16 +11,12 @@ test.describe('general landing page tests', () => {
test('get started link', async ({ page }) => { test('get started link', async ({ page }) => {
await page.getByRole('link', { name: 'Get started' }).click(); await page.getByRole('link', { name: 'Get started' }).click();
await page.waitForURL(DOCS_URL); await page.waitForURL('**/signup');
}); });
test('headings', async ({ page }) => { test('headings', async ({ page }) => {
await expect( await expect(page.getByRole('heading', { name: 'Frequently asked questions' })).toBeVisible();
page.getByRole('heading', { name: 'Frequently asked questions' }) await expect(page.getByRole('heading', { name: 'Some cool words' })).toBeVisible();
).toBeVisible();
await expect(
page.getByRole('heading', { name: 'Some cool words' })
).toBeVisible();
}); });
}); });
@@ -31,10 +25,7 @@ test.describe('cookie consent tests', () => {
await page.goto('/'); await page.goto('/');
}); });
test('cookie consent banner rejection does not set cc_cookie', async ({ test('cookie consent banner rejection does not set cc_cookie', async ({ context, page }) => {
context,
page,
}) => {
await page.$$('button:has-text("Reject all")'); await page.$$('button:has-text("Reject all")');
await page.click('button:has-text("Reject all")'); await page.click('button:has-text("Reject all")');
@@ -44,10 +35,7 @@ test.describe('cookie consent tests', () => {
expect(cookieObject.categories.includes('analytics')).toBeFalsy(); expect(cookieObject.categories.includes('analytics')).toBeFalsy();
}); });
test('cookie consent banner acceptance sets cc_cookie and _ga cookies', async ({ test('cookie consent banner acceptance sets cc_cookie and _ga cookies', async ({ context, page }) => {
context,
page,
}) => {
await page.$$('button:has-text("Accept all")'); await page.$$('button:has-text("Accept all")');
await page.click('button:has-text("Accept all")'); await page.click('button:has-text("Accept all")');

View File

@@ -1,5 +1,12 @@
import { test, expect, type Page } from '@playwright/test'; import { expect, test, type Page } from '@playwright/test';
import { signUserUp, logUserIn, createRandomUser, makeStripePayment, type User, acceptAllCookies } from './utils'; import {
acceptAllCookies,
createRandomUser,
logUserIn,
makeStripePayment,
signUserUp,
type User,
} from './utils';
let page: Page; let page: Page;
let testUser: User; let testUser: User;

View File

@@ -1,4 +1,4 @@
import { type Page, test, expect } from '@playwright/test'; import { expect, type Page } from '@playwright/test';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
export type User = { export type User = {
@@ -13,13 +13,11 @@ export const logUserIn = async ({ page, user }: { page: Page; user: User }) => {
await page.goto('/'); await page.goto('/');
await page.getByRole('link', { name: 'Log in' }).click(); await page.getByRole('link', { name: 'Log in' }).click();
await page.waitForURL('**/login', { await page.waitForURL('**/login', {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
}); });
await page.fill('input[name="email"]', user.email); await page.fill('input[name="email"]', user.email);
await page.fill('input[name="password"]', DEFAULT_PASSWORD); await page.fill('input[name="password"]', DEFAULT_PASSWORD);
const clickLogin = page.click('button:has-text("Log in")'); const clickLogin = page.click('button:has-text("Log in")');
@@ -39,7 +37,7 @@ export const logUserIn = async ({ page, user }: { page: Page; user: User }) => {
export const signUserUp = async ({ page, user }: { page: Page; user: User }) => { export const signUserUp = async ({ page, user }: { page: Page; user: User }) => {
await page.goto('/'); await page.goto('/');
await page.evaluate(() => { await page.evaluate(() => {
try { try {
const sessionId = localStorage.getItem('wasp:sessionId'); const sessionId = localStorage.getItem('wasp:sessionId');
@@ -59,7 +57,6 @@ export const signUserUp = async ({ page, user }: { page: Page; user: User }) =>
await page.click('text="go to signup"'); await page.click('text="go to signup"');
await page.fill('input[name="email"]', user.email); await page.fill('input[name="email"]', user.email);
await page.fill('input[name="password"]', DEFAULT_PASSWORD); await page.fill('input[name="password"]', DEFAULT_PASSWORD);
await page.click('button:has-text("Sign up")'); await page.click('button:has-text("Sign up")');