test: add basic e2e test suites for auth and app install

This commit is contained in:
Nicolas Meienberger 2023-04-25 20:15:14 +02:00 committed by Nicolas Meienberger
parent 5868ccb579
commit f389d51819
12 changed files with 232 additions and 3 deletions

View file

@ -31,7 +31,10 @@ module.exports = {
'react/jsx-props-no-spreading': 0,
'react/no-unused-prop-types': 0,
'react/button-has-type': 0,
'import/no-extraneous-dependencies': ['error', { devDependencies: ['esbuild.js', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/*.factory.{ts,tsx}', '**/mocks/**', 'tests/**', '**/*.d.ts'] }],
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: ['esbuild.js', 'e2e/**', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/*.factory.{ts,tsx}', '**/mocks/**', 'tests/**', '**/*.d.ts'] },
],
'no-underscore-dangle': 0,
'arrow-body-style': 0,
'class-methods-use-this': 0,

3
.gitignore vendored
View file

@ -59,3 +59,6 @@ traefik/shared
media
/state/
/test-results/
/playwright-report/
/playwright/.cache/

26
e2e/0001-register.spec.ts Normal file
View file

@ -0,0 +1,26 @@
import { test, expect } from '@playwright/test';
import { testUser } from './helpers/constants';
import { clearDatabase } from './helpers/db';
test('user should be redirected to /register', async ({ page }) => {
await clearDatabase();
await page.goto('/');
await page.waitForURL(/register/);
await expect(page.getByRole('heading', { name: 'Register your account' })).toBeVisible();
});
test('user can register a new account', async ({ page }) => {
await page.goto('/register');
await page.getByPlaceholder('you@example.com').click();
await page.getByPlaceholder('you@example.com').fill(testUser.email);
await page.getByPlaceholder('Your password', { exact: true }).fill(testUser.password);
await page.getByPlaceholder('Confirm your password').fill(testUser.password);
await page.getByRole('button', { name: 'Register' }).click();
await expect(page).toHaveTitle(/Dashboard/);
});

20
e2e/0002-login.spec.ts Normal file
View file

@ -0,0 +1,20 @@
import { test, expect } from '@playwright/test';
import { loginUser } from './fixtures/fixtures';
import { testUser } from './helpers/constants';
test('user can login and is redirected to the dashboard', async ({ page }) => {
await page.goto('/login');
await page.getByPlaceholder('you@example.com').fill(testUser.email);
await page.getByPlaceholder('Your password').fill(testUser.password);
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
test('user can logout', async ({ page }) => {
await loginUser(page);
await page.getByTestId('logout-button').click();
await expect(page.getByText('Login to your account')).toBeVisible();
});

51
e2e/0003-apps.spec.ts Normal file
View file

@ -0,0 +1,51 @@
import { test, expect } from '@playwright/test';
import { loginUser } from './fixtures/fixtures';
test.beforeEach(async ({ page, isMobile }) => {
await loginUser(page);
// Go to hello world app
if (isMobile) {
await page.getByRole('button', { name: 'menu' }).click();
}
await page.getByRole('link', { name: 'App store' }).click();
await page.getByPlaceholder('Search').fill('hello');
await page.getByRole('link', { name: 'Hello World' }).click();
});
test('user can install and uninstall app', async ({ page, context }) => {
// Install app
await page.getByRole('button', { name: 'Install' }).click();
await expect(page.getByText('Install Hello World')).toBeVisible();
await page.getByRole('button', { name: 'Install' }).click();
await expect(page.getByText('Installing')).toBeVisible();
await expect(page.getByText('Running')).toBeVisible({ timeout: 60000 });
await expect(page.getByText('App installed successfully')).toBeVisible();
const [newPage] = await Promise.all([context.waitForEvent('page'), page.getByRole('button', { name: 'Open' }).click()]);
await newPage.waitForLoadState();
await expect(newPage.getByText('Hello World')).toBeVisible();
await newPage.close();
// Stop app
await page.getByRole('button', { name: 'Stop' }).click();
await expect(page.getByText('Stop Hello World')).toBeVisible();
await page.getByRole('button', { name: 'Stop' }).click();
await expect(page.getByText('Stopping')).toBeVisible();
await expect(page.getByText('App stopped successfully')).toBeVisible({ timeout: 60000 });
// Uninstall app
await page.getByRole('button', { name: 'Remove' }).click();
await expect(page.getByText('Uninstall Hello World ?')).toBeVisible();
await page.getByRole('button', { name: 'Uninstall' }).click();
await expect(page.getByText('Uninstalling')).toBeVisible();
await expect(page.getByText('App uninstalled successfully')).toBeVisible({ timeout: 60000 });
});

12
e2e/fixtures/fixtures.ts Normal file
View file

@ -0,0 +1,12 @@
import { expect, Page } from '@playwright/test';
import { testUser } from '../helpers/constants';
export const loginUser = async (page: Page) => {
await page.goto('/login');
await page.getByPlaceholder('you@example.com').fill(testUser.email);
await page.getByPlaceholder('Your password').fill(testUser.password);
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeDefined();
};

4
e2e/helpers/constants.ts Normal file
View file

@ -0,0 +1,4 @@
export const testUser = {
email: 'tester@test.com',
password: 'password',
};

20
e2e/helpers/db.ts Normal file
View file

@ -0,0 +1,20 @@
import pg from 'pg';
import { getConfig } from '../../src/server/core/TipiConfig';
export const clearDatabase = async () => {
const pgClient = new pg.Client({
user: getConfig().postgresUsername,
host: 'localhost',
database: getConfig().postgresDatabase,
password: getConfig().postgresPassword,
port: getConfig().postgresPort,
});
await pgClient.connect();
// delete all data in table user
await pgClient.query('DELETE FROM "user"');
await pgClient.query('DELETE FROM "app"');
await pgClient.end();
};

View file

@ -0,0 +1,8 @@
/**
*
*/
async function globalSetup() {
console.log('Global setup...');
}
export default globalSetup;

82
playwright.config.ts Normal file
View file

@ -0,0 +1,82 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: require.resolve('./e2e/helpers/global-setup'),
testDir: './e2e',
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
video: 'on',
},
// timeout: 5000,
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run start:e2e',
url: 'http://127.0.0.1:3000',
reuseExistingServer: true,
},
});

View file

@ -30,7 +30,7 @@ export const Header: React.FC<IProps> = ({ isUpdateAvailable }) => {
return (
<header className="text-white navbar navbar-expand-md navbar-dark navbar-overlap d-print-none" data-bs-theme="dark">
<div className="container-xl">
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu">
<button aria-label="menu" className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu">
<span className="navbar-toggler-icon" />
</button>
<Link href="/" passHref>

View file

@ -18,7 +18,7 @@ const AppStoreTile: React.FC<{ app: App }> = ({ app }) => {
const t = useTranslations('apps.app-details');
return (
<Link className={clsx('cursor-pointer col-sm-6 col-lg-4 p-2 mt-4', styles.appTile)} href={`/app-store/${app.id}`} passHref>
<Link aria-label={app.name} className={clsx('cursor-pointer col-sm-6 col-lg-4 p-2 mt-4', styles.appTile)} href={`/app-store/${app.id}`} passHref>
<div key={app.id} className="d-flex overflow-hidden align-items-center py-2 ps-2">
<AppLogo className={styles.logo} id={app.id} />
<div className="card-body">