test: add basic e2e test suites for auth and app install
This commit is contained in:
parent
5868ccb579
commit
f389d51819
12 changed files with 232 additions and 3 deletions
|
@ -31,7 +31,10 @@ module.exports = {
|
||||||
'react/jsx-props-no-spreading': 0,
|
'react/jsx-props-no-spreading': 0,
|
||||||
'react/no-unused-prop-types': 0,
|
'react/no-unused-prop-types': 0,
|
||||||
'react/button-has-type': 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,
|
'no-underscore-dangle': 0,
|
||||||
'arrow-body-style': 0,
|
'arrow-body-style': 0,
|
||||||
'class-methods-use-this': 0,
|
'class-methods-use-this': 0,
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -59,3 +59,6 @@ traefik/shared
|
||||||
media
|
media
|
||||||
|
|
||||||
/state/
|
/state/
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|
26
e2e/0001-register.spec.ts
Normal file
26
e2e/0001-register.spec.ts
Normal 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
20
e2e/0002-login.spec.ts
Normal 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
51
e2e/0003-apps.spec.ts
Normal 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
12
e2e/fixtures/fixtures.ts
Normal 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
4
e2e/helpers/constants.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const testUser = {
|
||||||
|
email: 'tester@test.com',
|
||||||
|
password: 'password',
|
||||||
|
};
|
20
e2e/helpers/db.ts
Normal file
20
e2e/helpers/db.ts
Normal 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();
|
||||||
|
};
|
8
e2e/helpers/global-setup.ts
Normal file
8
e2e/helpers/global-setup.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function globalSetup() {
|
||||||
|
console.log('Global setup...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalSetup;
|
82
playwright.config.ts
Normal file
82
playwright.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
|
@ -30,7 +30,7 @@ export const Header: React.FC<IProps> = ({ isUpdateAvailable }) => {
|
||||||
return (
|
return (
|
||||||
<header className="text-white navbar navbar-expand-md navbar-dark navbar-overlap d-print-none" data-bs-theme="dark">
|
<header className="text-white navbar navbar-expand-md navbar-dark navbar-overlap d-print-none" data-bs-theme="dark">
|
||||||
<div className="container-xl">
|
<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" />
|
<span className="navbar-toggler-icon" />
|
||||||
</button>
|
</button>
|
||||||
<Link href="/" passHref>
|
<Link href="/" passHref>
|
||||||
|
|
|
@ -18,7 +18,7 @@ const AppStoreTile: React.FC<{ app: App }> = ({ app }) => {
|
||||||
const t = useTranslations('apps.app-details');
|
const t = useTranslations('apps.app-details');
|
||||||
|
|
||||||
return (
|
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">
|
<div key={app.id} className="d-flex overflow-hidden align-items-center py-2 ps-2">
|
||||||
<AppLogo className={styles.logo} id={app.id} />
|
<AppLogo className={styles.logo} id={app.id} />
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
|
Loading…
Reference in a new issue