mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-09-28 20:44:13 +02:00
Fix failing e2e tests (#460)
* fix broken tests * navbar fixes + return pricing * changes * re-remove pricing
This commit is contained in:
@@ -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}
|
||||||
|
@@ -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;
|
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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();
|
|
||||||
});
|
});
|
||||||
|
@@ -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")');
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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")');
|
||||||
|
Reference in New Issue
Block a user