diff --git a/.eslintrc.js b/.eslintrc.js index c9ddeba8..1a599492 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, diff --git a/.gitignore b/.gitignore index 9d5a90cf..647345af 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ traefik/shared media /state/ +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/e2e/0001-register.spec.ts b/e2e/0001-register.spec.ts new file mode 100644 index 00000000..37cccad8 --- /dev/null +++ b/e2e/0001-register.spec.ts @@ -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/); +}); diff --git a/e2e/0002-login.spec.ts b/e2e/0002-login.spec.ts new file mode 100644 index 00000000..839912d8 --- /dev/null +++ b/e2e/0002-login.spec.ts @@ -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(); +}); diff --git a/e2e/0003-apps.spec.ts b/e2e/0003-apps.spec.ts new file mode 100644 index 00000000..c33871c3 --- /dev/null +++ b/e2e/0003-apps.spec.ts @@ -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 }); +}); diff --git a/e2e/fixtures/fixtures.ts b/e2e/fixtures/fixtures.ts new file mode 100644 index 00000000..649f72e9 --- /dev/null +++ b/e2e/fixtures/fixtures.ts @@ -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(); +}; diff --git a/e2e/helpers/constants.ts b/e2e/helpers/constants.ts new file mode 100644 index 00000000..389803da --- /dev/null +++ b/e2e/helpers/constants.ts @@ -0,0 +1,4 @@ +export const testUser = { + email: 'tester@test.com', + password: 'password', +}; diff --git a/e2e/helpers/db.ts b/e2e/helpers/db.ts new file mode 100644 index 00000000..e8d2a44a --- /dev/null +++ b/e2e/helpers/db.ts @@ -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(); +}; diff --git a/e2e/helpers/global-setup.ts b/e2e/helpers/global-setup.ts new file mode 100644 index 00000000..0d102d5d --- /dev/null +++ b/e2e/helpers/global-setup.ts @@ -0,0 +1,8 @@ +/** + * + */ +async function globalSetup() { + console.log('Global setup...'); +} + +export default globalSetup; diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..6051636c --- /dev/null +++ b/playwright.config.ts @@ -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, + }, +}); diff --git a/src/client/components/ui/Header/Header.tsx b/src/client/components/ui/Header/Header.tsx index 79e51b9b..23b5e01e 100644 --- a/src/client/components/ui/Header/Header.tsx +++ b/src/client/components/ui/Header/Header.tsx @@ -30,7 +30,7 @@ export const Header: React.FC = ({ isUpdateAvailable }) => { return (
- diff --git a/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx b/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx index f7f0734a..3df092d3 100644 --- a/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx +++ b/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx @@ -18,7 +18,7 @@ const AppStoreTile: React.FC<{ app: App }> = ({ app }) => { const t = useTranslations('apps.app-details'); return ( - +